diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 541ebab28..83af6db51 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -35,10 +35,6 @@ android { applicationIdSuffix = ".dev" versionNameSuffix = "-dev" buildConfigField("String", "ENVIRONMENT", "\"dev\"") - - compileOptions { - isCoreLibraryDesugaringEnabled = true - } } create("prod") { @@ -144,7 +140,7 @@ dependencies { implementation(libs.material) implementation(libs.javax.inject) - coreLibraryDesugaring(libs.desugar.jdk.libs) +// coreLibraryDesugaring(libs.desugar.jdk.libs) implementation(libs.okhttp) implementation(libs.okhttp.interceptor.logging) diff --git a/app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt b/app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt index 9ec9c41d0..b375b0ff5 100644 --- a/app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt +++ b/app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt @@ -8,6 +8,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -22,12 +23,16 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.serialization.Serializable import team.aliens.dms.android.core.designsystem.snackbar.DmsSnackBar import team.aliens.dms.android.core.designsystem.snackbar.DmsSnackBarVisuals +import team.aliens.dms.android.core.ui.navigation.LocalResultStore +import team.aliens.dms.android.core.ui.navigation.rememberResultStore import team.aliens.dms.android.feature.main.application.navigation.ApplicationRoute import team.aliens.dms.android.feature.main.home.navigation.HomeRoute import team.aliens.dms.android.feature.main.mypage.navigation.MyPageRoute import team.aliens.dms.android.feature.meal.navigation.MealRoute import team.aliens.dms.android.feature.onboarding.navigation.OnboardingRoute +import team.aliens.dms.android.feature.remain.navigation.RemainApplicationRoute import team.aliens.dms.android.feature.signin.navigation.SignInRoute +import team.aliens.dms.android.feature.vote.navigation.VoteRoute @Serializable data object OnboardingScreenNav : NavKey @@ -44,6 +49,12 @@ data object MealScreenNav : NavKey @Serializable data object ApplicationScreenNav : NavKey +@Serializable +data class VoteScreenNav(val title: String, val startTime: String, val endTime: String) : NavKey + +@Serializable +data object RemainScreenNav : NavKey + @Serializable data object MyPageScreenNav : NavKey @@ -58,6 +69,7 @@ fun DmsApp( val isJwtAvailableState by isJwtAvailable.collectAsState() val backStack = rememberNavBackStack(OnboardingScreenNav) + val resultStore = rememberResultStore() val currentScreen = backStack.lastOrNull() val shouldShowBottomBar = currentScreen in listOf( HomeScreenNav, @@ -98,13 +110,14 @@ fun DmsApp( } } ) { paddingValues -> - NavDisplay( - modifier = Modifier - .padding(paddingValues) - .navigationBarsPadding(), - backStack = backStack, - onBack = { backStack.removeLastOrNull() }, - entryProvider = entryProvider { + CompositionLocalProvider(LocalResultStore provides resultStore) { + NavDisplay( + modifier = Modifier + .padding(paddingValues) + .navigationBarsPadding(), + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + entryProvider = entryProvider { entry { OnboardingRoute( navigateToSignIn = { @@ -136,18 +149,53 @@ fun DmsApp( ) } entry { - ApplicationRoute() + ApplicationRoute( + onNavigateRemainApplication = { + backStack.add(RemainScreenNav) + }, + onNavigateOutingApplication = {}, + onNavigateVolunteerApplication = {}, + onNavigateVote = { + backStack.add(VoteScreenNav(it.topicName, it.startTime.toString(), it.endTime.toString())) + }, + onShowSnackBar = { snackBarType, message -> + appState.showSnackBar(snackBarType, message) + }, + ) + } + entry { voteNav -> + VoteRoute( + title = voteNav.title, + startTime = voteNav.startTime, + endTime = voteNav.endTime, + onNavigateBack = { backStack.remove(VoteScreenNav(voteNav.title, voteNav.startTime, voteNav.endTime)) }, + onShowSnackBar = { snackBarType, message -> + appState.showSnackBar(snackBarType, message) + } + ) + } + entry { + RemainApplicationRoute( + onNavigateBack = { title -> + resultStore.setResult("remain_application_result", title) + backStack.remove(RemainScreenNav) + }, + onShowSnackBar = { snackBarType, message -> + appState.showSnackBar(snackBarType, message) + } + ) } entry { MyPageRoute() } entry { MealRoute( - onNavigateBack = { backStack.removeLastOrNull() } + onNavigateBack = { backStack.remove(MealScreenNav) } ) } }, - ) + ) + } SnackbarHost( modifier = Modifier .statusBarsPadding() diff --git a/buildSrc/src/main/kotlin/ProjectProperties.kt b/buildSrc/src/main/kotlin/ProjectProperties.kt index daef21eb6..22e30eb61 100644 --- a/buildSrc/src/main/kotlin/ProjectProperties.kt +++ b/buildSrc/src/main/kotlin/ProjectProperties.kt @@ -1,6 +1,6 @@ object ProjectProperties { const val COMPILE_SDK = 36 - const val MIN_SDK = 24 + const val MIN_SDK = 26 const val TARGET_SDK = 36 const val VERSION_CODE = 28 const val VERSION_NAME = "1.5.3" diff --git a/core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/button/Button.kt b/core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/button/Button.kt index b2cddb476..5c53513a7 100644 --- a/core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/button/Button.kt +++ b/core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/button/Button.kt @@ -4,9 +4,15 @@ import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -21,6 +27,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import team.aliens.dms.android.core.designsystem.DmsTheme import team.aliens.dms.android.core.designsystem.bodyM @@ -308,8 +315,6 @@ fun DmsButton( PaddingValues(horizontal = 20.dp, vertical = 16.dp) } - // val buttonShape = if (buttonType == ButtonType.Rounded) RoundedCornerShape(24.dp) else shape - BasicButton( modifier = modifier, backgroundColor = backgroundColor, @@ -349,3 +354,35 @@ fun DmsButton( } } } +@Composable +fun DmsLayeredButton( + modifier: Modifier = Modifier, + text: String, + buttonType: ButtonType, + buttonColor: ButtonColor, + enabled: Boolean = true, + shape: RoundedCornerShape, + backgroundColor: Color = DmsTheme.colorScheme.surfaceVariant, + layerOffset: Dp = 24.dp, + isLoading: Boolean, + onClick: () -> Unit, +) { + Box( + modifier = modifier + .background(color = Color.White, shape = shape) + .windowInsetsPadding(WindowInsets.navigationBars) + .padding(layerOffset), + ) { + DmsButton( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.Center), + text = text, + buttonType = buttonType, + buttonColor = buttonColor, + enabled = enabled, + isLoading = isLoading, + onClick = onClick, + ) + } +} diff --git a/core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/card/DmsApplicationCard.kt b/core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/card/DmsApplicationCard.kt new file mode 100644 index 000000000..4172b55e0 --- /dev/null +++ b/core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/card/DmsApplicationCard.kt @@ -0,0 +1,134 @@ +package team.aliens.dms.android.core.designsystem.card + +import androidx.annotation.DrawableRes +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import team.aliens.dms.android.core.designsystem.DmsTheme +import team.aliens.dms.android.core.designsystem.bodyB +import team.aliens.dms.android.core.designsystem.endPadding +import team.aliens.dms.android.core.designsystem.foundation.DmsIcon +import team.aliens.dms.android.core.designsystem.labelB +import team.aliens.dms.android.core.designsystem.labelM +import team.aliens.dms.android.core.designsystem.util.clickable + +@Composable +fun DmsApplicationCard( + modifier: Modifier = Modifier, + title: String, + description: String? = null, + period: String? = null, + appliedTitle: String? = null, + @DrawableRes iconRes: Int, + isSelected: Boolean = false, + onClick: () -> Unit, +) { + val borderColor by animateColorAsState( + targetValue = if (isSelected) { + DmsTheme.colorScheme.onPrimaryContainer + } else { + DmsTheme.colorScheme.surfaceTint + }, + ) + Column( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(32.dp)) + .background(DmsTheme.colorScheme.surfaceTint) + .clickable(onClick = onClick) + .border( + width = 2.dp, + color = borderColor, + shape = RoundedCornerShape(32.dp), + ) + .padding(horizontal = 16.dp, vertical = 24.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + modifier = Modifier.size(32.dp), + painter = painterResource(iconRes), + contentDescription = null, + ) + Text( + modifier = Modifier.padding(start = 8.dp), + text = title, + style = DmsTheme.typography.bodyB, + color = DmsTheme.colorScheme.inverseOnSurface, + ) + Spacer(modifier = Modifier.weight(1f)) + if (description == null && appliedTitle != null) { + AppliedTitleText( + modifier = Modifier.endPadding(16.dp), + appliedTitle = appliedTitle, + ) + } + Icon( + painter = painterResource(DmsIcon.Forward), + tint = DmsTheme.colorScheme.scrim, + contentDescription = null, + ) + } + period?.let { + Text( + text = period, + style = DmsTheme.typography.labelM, + color = DmsTheme.colorScheme.onPrimaryContainer, + ) + } + description?.let { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = description, + style = DmsTheme.typography.labelM, + color = DmsTheme.colorScheme.inverseSurface, + ) + Spacer(modifier = Modifier.weight(1f)) + appliedTitle?.let { + AppliedTitleText(appliedTitle = appliedTitle) + } + } + } + } +} + +@Composable +private fun AppliedTitleText( + modifier: Modifier = Modifier, + appliedTitle: String, +) { + Text( + modifier = modifier + .background( + color = DmsTheme.colorScheme.primary, + shape = RoundedCornerShape(6.dp), + ) + .padding(horizontal = 22.dp, vertical = 8.dp), + text = appliedTitle, + style = DmsTheme.typography.labelB, + color = DmsTheme.colorScheme.onPrimaryContainer, + ) +} diff --git a/core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/modifier/DmsShadowModifier.kt b/core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/modifier/DmsShadowModifier.kt new file mode 100644 index 000000000..eab988009 --- /dev/null +++ b/core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/modifier/DmsShadowModifier.kt @@ -0,0 +1,60 @@ +package team.aliens.dms.android.core.designsystem.modifier + +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.dp + +@Composable +fun Modifier.dmsShadowModifier( + dmsShadowType: DmsShadowType, + shape: Shape = CircleShape, +): Modifier { + return when (dmsShadowType) { + DmsShadowType.Light10 -> this.dmsDropShadow( + shape = shape, + color = Color.Black.copy(alpha = 0.08f), + blur = 10.dp, + offsetX = 0.dp, + offsetY = 4.dp, + ) + + DmsShadowType.Light20 -> this.dmsDropShadow( + shape = shape, + color = Color.Black.copy(alpha = 0.1f), + blur = 15.dp, + offsetX = 0.dp, + offsetY = 1.dp, + ) + + DmsShadowType.Standard -> this.dmsDropShadow( + shape = shape, + color = Color.Black.copy(alpha = 0.19f), + blur = 20.dp, + offsetX = 0.dp, + offsetY = 3.dp, + ) + + DmsShadowType.Dark10 -> this.dmsDropShadow( + shape = shape, + color = Color.Black.copy(alpha = 0.25f), + blur = 14.dp, + offsetX = 0.dp, + offsetY = 14.dp, + ) + + DmsShadowType.Dark20 -> this.dmsDropShadow( + shape = shape, + color = Color.Black.copy(alpha = 0.30f), + blur = 19.dp, + offsetX = 0.dp, + offsetY = 19.dp, + ) + } +} + +enum class DmsShadowType { + Light10, Light20, Standard, Dark10, Dark20 +} diff --git a/core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/tab/DmsTab.kt b/core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/tab/DmsTab.kt new file mode 100644 index 000000000..ace886a19 --- /dev/null +++ b/core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/tab/DmsTab.kt @@ -0,0 +1,44 @@ +package team.aliens.dms.android.core.designsystem.tab + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.Tab +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import team.aliens.dms.android.core.designsystem.DmsTheme +import team.aliens.dms.android.core.designsystem.lBodyB +import team.aliens.dms.android.core.designsystem.lBodyM + +@Composable +fun DmsTab( + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + text: String, + icon: @Composable (() -> Unit)? = null, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + selectedContentColor: Color = DmsTheme.colorScheme.tertiaryContainer, + unselectedContentColor: Color = DmsTheme.colorScheme.inverseSurface, +) { + val (textStyle, textColor) = if (selected) DmsTheme.typography.lBodyB to selectedContentColor else DmsTheme.typography.lBodyM to unselectedContentColor + Tab( + selected = selected, + onClick = onClick, + modifier = modifier, + enabled = enabled, + text = { + Text( + text = text, + style = textStyle, + color = textColor, + ) + }, + icon = icon, + interactionSource = interactionSource, + selectedContentColor = selectedContentColor, + unselectedContentColor = unselectedContentColor, + ) +} diff --git a/core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/tab/DmsTabRow.kt b/core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/tab/DmsTabRow.kt new file mode 100644 index 000000000..82a32d876 --- /dev/null +++ b/core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/tab/DmsTabRow.kt @@ -0,0 +1,55 @@ +package team.aliens.dms.android.core.designsystem.tab + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.TabPosition +import androidx.compose.material3.TabRow +import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import team.aliens.dms.android.core.designsystem.DmsTheme + +@Composable +fun DmsTabRow( + selectedTabIndex: Int, + modifier: Modifier = Modifier, + containerColor: Color = DmsTheme.colorScheme.background, + contentColor: Color = DmsTheme.colorScheme.onBackground, + indicator: @Composable (tabPositions: List) -> Unit = + @Composable { tabPositions -> + if (selectedTabIndex < tabPositions.size) { + TabRowDefaults.SecondaryIndicator( + modifier = Modifier + .tabIndicatorOffset(tabPositions[selectedTabIndex]) + .clip( + shape = RoundedCornerShape( + topStart = 12.dp, + topEnd = 12.dp, + ), + ), + color = DmsTheme.colorScheme.onPrimaryContainer, + height = 2.dp, + ) + } + }, + divider: @Composable () -> Unit = @Composable { + HorizontalDivider( + color = DmsTheme.colorScheme.onSurfaceVariant, + ) + }, + tabs: @Composable () -> Unit, +) { + TabRow( + selectedTabIndex = selectedTabIndex, + modifier = modifier, + containerColor = containerColor, + contentColor = contentColor, + indicator = indicator, + divider = divider, + tabs = tabs, + ) +} diff --git a/core/design-system/src/dev/res/drawable/ic_approve.xml b/core/design-system/src/dev/res/drawable/ic_approve.xml new file mode 100644 index 000000000..436407aaf --- /dev/null +++ b/core/design-system/src/dev/res/drawable/ic_approve.xml @@ -0,0 +1,11 @@ + + + diff --git a/core/design-system/src/dev/res/drawable/ic_oppose.xml b/core/design-system/src/dev/res/drawable/ic_oppose.xml new file mode 100644 index 000000000..939d463a9 --- /dev/null +++ b/core/design-system/src/dev/res/drawable/ic_oppose.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/design-system/src/dev/res/drawable/img_bus.png b/core/design-system/src/dev/res/drawable/img_bus.png new file mode 100644 index 000000000..d1e5dd860 Binary files /dev/null and b/core/design-system/src/dev/res/drawable/img_bus.png differ diff --git a/core/design-system/src/dev/res/drawable/img_choice.png b/core/design-system/src/dev/res/drawable/img_choice.png new file mode 100644 index 000000000..c5a956036 Binary files /dev/null and b/core/design-system/src/dev/res/drawable/img_choice.png differ diff --git a/core/design-system/src/dev/res/drawable/img_home.png b/core/design-system/src/dev/res/drawable/img_home.png new file mode 100644 index 000000000..17a46997b Binary files /dev/null and b/core/design-system/src/dev/res/drawable/img_home.png differ diff --git a/core/design-system/src/dev/res/drawable/img_model_student.png b/core/design-system/src/dev/res/drawable/img_model_student.png new file mode 100644 index 000000000..571624afb Binary files /dev/null and b/core/design-system/src/dev/res/drawable/img_model_student.png differ diff --git a/core/design-system/src/dev/res/drawable/img_night_bus.png b/core/design-system/src/dev/res/drawable/img_night_bus.png new file mode 100644 index 000000000..c88881320 Binary files /dev/null and b/core/design-system/src/dev/res/drawable/img_night_bus.png differ diff --git a/core/design-system/src/dev/res/drawable/img_outing.png b/core/design-system/src/dev/res/drawable/img_outing.png new file mode 100644 index 000000000..422d15ae1 Binary files /dev/null and b/core/design-system/src/dev/res/drawable/img_outing.png differ diff --git a/core/design-system/src/dev/res/drawable/img_percent.png b/core/design-system/src/dev/res/drawable/img_percent.png new file mode 100644 index 000000000..d15a9eb09 Binary files /dev/null and b/core/design-system/src/dev/res/drawable/img_percent.png differ diff --git a/core/design-system/src/dev/res/drawable/img_small_home.png b/core/design-system/src/dev/res/drawable/img_small_home.png new file mode 100644 index 000000000..a83c3cc79 Binary files /dev/null and b/core/design-system/src/dev/res/drawable/img_small_home.png differ diff --git a/core/design-system/src/dev/res/drawable/img_student_tag.png b/core/design-system/src/dev/res/drawable/img_student_tag.png new file mode 100644 index 000000000..3f3f2e1cc Binary files /dev/null and b/core/design-system/src/dev/res/drawable/img_student_tag.png differ diff --git a/core/design-system/src/dev/res/drawable/img_volunteer.png b/core/design-system/src/dev/res/drawable/img_volunteer.png new file mode 100644 index 000000000..a33d3e7ce Binary files /dev/null and b/core/design-system/src/dev/res/drawable/img_volunteer.png differ diff --git a/core/jwt/src/main/java/team/aliens/dms/android/core/jwt/JwtProviderImpl.kt b/core/jwt/src/main/java/team/aliens/dms/android/core/jwt/JwtProviderImpl.kt index 80f3d4b38..4248fc837 100644 --- a/core/jwt/src/main/java/team/aliens/dms/android/core/jwt/JwtProviderImpl.kt +++ b/core/jwt/src/main/java/team/aliens/dms/android/core/jwt/JwtProviderImpl.kt @@ -112,6 +112,16 @@ internal class JwtProviderImpl @Inject constructor( jwtReissueManager(refreshToken = cachedRefreshToken.value) }.onSuccess { tokens -> this@JwtProviderImpl.updateTokens(tokens = tokens) + }.onFailure { exception -> + when { + exception is retrofit2.HttpException && exception.code() == 401 -> { + this@JwtProviderImpl.clearCaches() + } + exception is CannotUseRefreshTokenException -> { + this@JwtProviderImpl.clearCaches() + } + else -> {} + } } this@JwtProviderImpl.refreshTokenAbility() } diff --git a/core/ui/src/dev/java/team/aliens/dms/android/core/ui/navigation/ResultStore.kt b/core/ui/src/dev/java/team/aliens/dms/android/core/ui/navigation/ResultStore.kt new file mode 100644 index 000000000..9bea507d2 --- /dev/null +++ b/core/ui/src/dev/java/team/aliens/dms/android/core/ui/navigation/ResultStore.kt @@ -0,0 +1,84 @@ +package team.aliens.dms.android.core.ui.navigation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.ProvidedValue +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable + +/** + * Local for storing results in a [ResultStore] + */ +object LocalResultStore { + private val LocalResultStore: ProvidableCompositionLocal = + compositionLocalOf { null } + + /** + * The current [ResultStore] + */ + val current: ResultStore + @Composable + get() = LocalResultStore.current ?: error("No ResultStore has been provided") + + /** + * Provides a [ResultStore] to the composition + */ + infix fun provides( + store: ResultStore + ): ProvidedValue { + return LocalResultStore.provides(store) + } +} + +/** + * Provides a [ResultStore] that will be remembered across configuration changes. + */ +@Composable +fun rememberResultStore(): ResultStore { + return rememberSaveable(saver = ResultStoreSaver()) { + ResultStore() + } +} + +/** + * A store for passing results between multiple sets of screens. + * + * It provides a solution for state based results. + */ +class ResultStore { + + /** + * Map from the result key to a mutable state of the result. + */ + val resultStateMap: MutableMap> = mutableMapOf() + + /** + * Retrieves the current result of the given resultKey. + */ + inline fun getResultState(resultKey: String = T::class.toString()) = + resultStateMap[resultKey]?.value as T + + /** + * Sets the result for the given resultKey. + */ + inline fun setResult(resultKey: String = T::class.toString(), result: T) { + resultStateMap[resultKey] = mutableStateOf(result) + } + + /** + * Removes all results associated with the given key from the store. + */ + inline fun removeResult(resultKey: String = T::class.toString()) { + resultStateMap.remove(resultKey) + } +} + +/** Saver to save and restore the NavController across config change and process death. */ +private fun ResultStoreSaver(): Saver = + Saver( + save = { it.resultStateMap }, + restore = { ResultStore().apply { resultStateMap.putAll(it) } }, + ) \ No newline at end of file diff --git a/core/ui/src/dev/java/team/aliens/dms/android/core/ui/util/Date.kt b/core/ui/src/dev/java/team/aliens/dms/android/core/ui/util/Date.kt new file mode 100644 index 000000000..e0cf9f8f3 --- /dev/null +++ b/core/ui/src/dev/java/team/aliens/dms/android/core/ui/util/Date.kt @@ -0,0 +1,7 @@ +package team.aliens.dms.android.core.ui.util + +import java.time.LocalDateTime + +fun LocalDateTime.toDateString(): String { + return "${this.year}년 ${this.monthValue}월 ${this.dayOfMonth}일" +} diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 0cd43d399..f2c1c9689 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -7,6 +7,7 @@ plugins { alias(libs.plugins.hilt) alias(libs.plugins.ksp) alias(libs.plugins.ktlint.gradle) + alias(libs.plugins.serialization) } android { diff --git a/data/src/dev/kotlin/team.aliens.dms.android.data/remain/di/RepositoryModule.kt b/data/src/dev/kotlin/team.aliens.dms.android.data/remain/di/RepositoryModule.kt new file mode 100644 index 000000000..b8d133e0c --- /dev/null +++ b/data/src/dev/kotlin/team.aliens.dms.android.data/remain/di/RepositoryModule.kt @@ -0,0 +1,18 @@ +package team.aliens.dms.android.data.remain.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import team.aliens.dms.android.data.remain.repository.RemainsRepository +import team.aliens.dms.android.data.remain.repository.RemainsRepositoryImpl +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal abstract class RepositoryModule { + + @Binds + @Singleton + abstract fun bindRemainsRepository(impl: RemainsRepositoryImpl): RemainsRepository +} diff --git a/data/src/dev/kotlin/team.aliens.dms.android.data/remain/mapper/RemainsMapper.kt b/data/src/dev/kotlin/team.aliens.dms.android.data/remain/mapper/RemainsMapper.kt new file mode 100644 index 000000000..e4b91c2ac --- /dev/null +++ b/data/src/dev/kotlin/team.aliens.dms.android.data/remain/mapper/RemainsMapper.kt @@ -0,0 +1,33 @@ +package team.aliens.dms.android.data.remain.mapper + +import team.aliens.dms.android.data.remain.model.AppliedRemainsOption +import team.aliens.dms.android.data.remain.model.RemainsApplicationTime +import team.aliens.dms.android.data.remain.model.RemainsOption +import team.aliens.dms.android.network.remains.model.FetchAppliedRemainsOptionResponse +import team.aliens.dms.android.network.remains.model.FetchRemainsApplicationTimeResponse +import team.aliens.dms.android.network.remains.model.FetchRemainsOptionsResponse + +internal fun FetchAppliedRemainsOptionResponse.toModel(): AppliedRemainsOption = + AppliedRemainsOption( + id = this.remainsOptionId, + title = this.title, + ) + +internal fun FetchRemainsApplicationTimeResponse.toModel(): RemainsApplicationTime = + RemainsApplicationTime( + startDayOfWeek = this.startDayOfWeek, + startTime = this.startTime, + endDayOfWeek = this.endDayOfWeek, + endTime = this.endTime, + ) + +internal fun FetchRemainsOptionsResponse.toModel(): List = + this.remainsOptionResponse.map(FetchRemainsOptionsResponse.RemainsOptionResponse::toModel) + +private fun FetchRemainsOptionsResponse.RemainsOptionResponse.toModel(): RemainsOption = + RemainsOption( + id = this.id, + title = this.title, + description = this.description, + applied = this.applied, + ) diff --git a/data/src/dev/kotlin/team.aliens.dms.android.data/remain/model/AppliedRemainsOption.kt b/data/src/dev/kotlin/team.aliens.dms.android.data/remain/model/AppliedRemainsOption.kt new file mode 100644 index 000000000..959a2fa13 --- /dev/null +++ b/data/src/dev/kotlin/team.aliens.dms.android.data/remain/model/AppliedRemainsOption.kt @@ -0,0 +1,8 @@ +package team.aliens.dms.android.data.remain.model + +import java.util.UUID + +data class AppliedRemainsOption( + val id: UUID, + val title: String, +) diff --git a/data/src/dev/kotlin/team.aliens.dms.android.data/remain/model/RemainsApplicationTime.kt b/data/src/dev/kotlin/team.aliens.dms.android.data/remain/model/RemainsApplicationTime.kt new file mode 100644 index 000000000..c128e7f76 --- /dev/null +++ b/data/src/dev/kotlin/team.aliens.dms.android.data/remain/model/RemainsApplicationTime.kt @@ -0,0 +1,10 @@ +package team.aliens.dms.android.data.remain.model + +import java.time.DayOfWeek + +data class RemainsApplicationTime( + val startDayOfWeek: DayOfWeek = DayOfWeek.SUNDAY, + val startTime: String = "", + val endDayOfWeek: DayOfWeek = DayOfWeek.SUNDAY, + val endTime: String = "", +) diff --git a/data/src/dev/kotlin/team.aliens.dms.android.data/remain/model/RemainsOption.kt b/data/src/dev/kotlin/team.aliens.dms.android.data/remain/model/RemainsOption.kt new file mode 100644 index 000000000..3fed35775 --- /dev/null +++ b/data/src/dev/kotlin/team.aliens.dms.android.data/remain/model/RemainsOption.kt @@ -0,0 +1,10 @@ +package team.aliens.dms.android.data.remain.model + +import java.util.UUID + +data class RemainsOption( + val id: UUID?, + val title: String, + val description: String, + val applied: Boolean, +) diff --git a/data/src/dev/kotlin/team.aliens.dms.android.data/remain/repository/RemainsRepository.kt b/data/src/dev/kotlin/team.aliens.dms.android.data/remain/repository/RemainsRepository.kt new file mode 100644 index 000000000..11a345d1d --- /dev/null +++ b/data/src/dev/kotlin/team.aliens.dms.android.data/remain/repository/RemainsRepository.kt @@ -0,0 +1,17 @@ +package team.aliens.dms.android.data.remain.repository + +import team.aliens.dms.android.data.remain.model.AppliedRemainsOption +import team.aliens.dms.android.data.remain.model.RemainsApplicationTime +import team.aliens.dms.android.data.remain.model.RemainsOption +import java.util.UUID + +abstract class RemainsRepository { + + abstract suspend fun updateRemainsOption(optionId: UUID): Result + + abstract suspend fun fetchAppliedRemainsOption(): Result + + abstract suspend fun fetchRemainsApplicationTime(): Result + + abstract suspend fun fetchRemainsOptions(): Result> +} diff --git a/data/src/dev/kotlin/team.aliens.dms.android.data/remain/repository/RemainsRepositoryImpl.kt b/data/src/dev/kotlin/team.aliens.dms.android.data/remain/repository/RemainsRepositoryImpl.kt new file mode 100644 index 000000000..090dff6de --- /dev/null +++ b/data/src/dev/kotlin/team.aliens.dms.android.data/remain/repository/RemainsRepositoryImpl.kt @@ -0,0 +1,30 @@ +package team.aliens.dms.android.data.remain.repository + +import team.aliens.dms.android.data.remain.mapper.toModel +import team.aliens.dms.android.data.remain.model.AppliedRemainsOption +import team.aliens.dms.android.data.remain.model.RemainsApplicationTime +import team.aliens.dms.android.data.remain.model.RemainsOption +import team.aliens.dms.android.network.remains.datasource.NetworkRemainsDataSource +import java.util.UUID +import javax.inject.Inject + +internal class RemainsRepositoryImpl @Inject constructor( + private val networkRemainsDataSource: NetworkRemainsDataSource, +) : RemainsRepository() { + + override suspend fun updateRemainsOption(optionId: UUID): Result = runCatching { + networkRemainsDataSource.updateRemainsOption(optionId) + } + + override suspend fun fetchAppliedRemainsOption(): Result = runCatching { + networkRemainsDataSource.fetchAppliedRemainsOption().toModel() + } + + override suspend fun fetchRemainsApplicationTime(): Result = runCatching { + networkRemainsDataSource.fetchRemainsApplicationTime().toModel() + } + + override suspend fun fetchRemainsOptions(): Result> = runCatching { + networkRemainsDataSource.fetchRemainsOptions().toModel() + } +} diff --git a/data/src/dev/kotlin/team.aliens.dms.android.data/voting/di/RepositoryModule.kt b/data/src/dev/kotlin/team.aliens.dms.android.data/voting/di/RepositoryModule.kt new file mode 100644 index 000000000..30c50ce9d --- /dev/null +++ b/data/src/dev/kotlin/team.aliens.dms.android.data/voting/di/RepositoryModule.kt @@ -0,0 +1,18 @@ +package team.aliens.dms.android.data.voting.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import team.aliens.dms.android.data.voting.repository.VotingRepository +import team.aliens.dms.android.data.voting.repository.VotingRepositoryImpl +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal abstract class RepositoryModule { + + @Binds + @Singleton + abstract fun bindVotingRepository(impl: VotingRepositoryImpl): VotingRepository +} diff --git a/data/src/dev/kotlin/team.aliens.dms.android.data/voting/mapper/VotingMapper.kt b/data/src/dev/kotlin/team.aliens.dms.android.data/voting/mapper/VotingMapper.kt new file mode 100644 index 000000000..4bd632c8b --- /dev/null +++ b/data/src/dev/kotlin/team.aliens.dms.android.data/voting/mapper/VotingMapper.kt @@ -0,0 +1,45 @@ +package team.aliens.dms.android.data.voting.mapper + +import team.aliens.dms.android.data.student.model.Student +import team.aliens.dms.android.data.voting.model.AllVoteSearch +import team.aliens.dms.android.data.voting.model.Vote +import team.aliens.dms.android.data.voting.model.VotingItem +import team.aliens.dms.android.network.voting.model.FetchAllVoteSearchResponse +import team.aliens.dms.android.network.voting.model.FetchCheckVotingItemResponse +import team.aliens.dms.android.network.voting.model.FetchModelStudentCandidatesResponse +import team.aliens.dms.android.shared.date.toLocalDateTime +import java.util.UUID + +internal fun FetchAllVoteSearchResponse.toModel(): List = + this.votingTopics.map(FetchAllVoteSearchResponse.VoteSearchResponse::toModel) + +private fun FetchAllVoteSearchResponse.VoteSearchResponse.toModel(): AllVoteSearch = + AllVoteSearch( + id = UUID.fromString(this.id), + topicName = this.topicName, + description = this.description, + startTime = this.startTime.toLocalDateTime(), + endTime = this.endTime.toLocalDateTime(), + voteType = Vote.valueOf(this.voteType), + isVoted = this.isVoted, + ) + +internal fun FetchCheckVotingItemResponse.toModel(): List = + this.votingOptions.map(FetchCheckVotingItemResponse.VotingItemResponse::toModel) + +private fun FetchCheckVotingItemResponse.VotingItemResponse.toModel(): VotingItem = + VotingItem( + id = id, + votingOptionName = votingOptionName, + ) + +internal fun FetchModelStudentCandidatesResponse.toModel(): List = + this.students.map(FetchModelStudentCandidatesResponse.ModelStudentCandidatesResponse::toModel) + +private fun FetchModelStudentCandidatesResponse.ModelStudentCandidatesResponse.toModel(): Student = + Student( + id = this.id, + gradeClassNumber = this.studentGcn.toString(), + name = this.name, + profileImageUrl = this.profileImageUrl, + ) diff --git a/data/src/dev/kotlin/team.aliens.dms.android.data/voting/model/AllVoteSearch.kt b/data/src/dev/kotlin/team.aliens.dms.android.data/voting/model/AllVoteSearch.kt new file mode 100644 index 000000000..4ff013b7e --- /dev/null +++ b/data/src/dev/kotlin/team.aliens.dms.android.data/voting/model/AllVoteSearch.kt @@ -0,0 +1,15 @@ +package team.aliens.dms.android.data.voting.model + +import team.aliens.dms.android.shared.date.util.now +import java.time.LocalDateTime +import java.util.UUID + +data class AllVoteSearch( + val id: UUID = UUID.randomUUID(), + val topicName: String = "", + val description: String = "", + val startTime: LocalDateTime = now, + val endTime: LocalDateTime = now, + val voteType: Vote = Vote.STUDENT_VOTE, + val isVoted: Boolean = false, +) diff --git a/data/src/dev/kotlin/team.aliens.dms.android.data/voting/model/ModelStudentCandidates.kt b/data/src/dev/kotlin/team.aliens.dms.android.data/voting/model/ModelStudentCandidates.kt new file mode 100644 index 000000000..8c47e5d2f --- /dev/null +++ b/data/src/dev/kotlin/team.aliens.dms.android.data/voting/model/ModelStudentCandidates.kt @@ -0,0 +1,10 @@ +package team.aliens.dms.android.data.voting.model + +import java.util.UUID + +data class ModelStudentCandidates( + val id: UUID, + val studentGcn: Long, + val name: String, + val profileImageUrl: String, +) diff --git a/data/src/dev/kotlin/team.aliens.dms.android.data/voting/model/StudentGcnInfo.kt b/data/src/dev/kotlin/team.aliens.dms.android.data/voting/model/StudentGcnInfo.kt new file mode 100644 index 000000000..ab8fdda6e --- /dev/null +++ b/data/src/dev/kotlin/team.aliens.dms.android.data/voting/model/StudentGcnInfo.kt @@ -0,0 +1,6 @@ +package team.aliens.dms.android.data.voting.model + +data class StudentGcnInfo( + val studentGcn: String, + val studentFilterId: Int, +) diff --git a/data/src/dev/kotlin/team.aliens.dms.android.data/voting/model/Vote.kt b/data/src/dev/kotlin/team.aliens.dms.android.data/voting/model/Vote.kt new file mode 100644 index 000000000..2fe4e5790 --- /dev/null +++ b/data/src/dev/kotlin/team.aliens.dms.android.data/voting/model/Vote.kt @@ -0,0 +1,6 @@ +package team.aliens.dms.android.data.voting.model + +enum class Vote { + MODEL_STUDENT_VOTE, APPROVAL_VOTE, STUDENT_VOTE, OPTION_VOTE + ; +} diff --git a/data/src/dev/kotlin/team.aliens.dms.android.data/voting/model/VotingItem.kt b/data/src/dev/kotlin/team.aliens.dms.android.data/voting/model/VotingItem.kt new file mode 100644 index 000000000..5409c5f13 --- /dev/null +++ b/data/src/dev/kotlin/team.aliens.dms.android.data/voting/model/VotingItem.kt @@ -0,0 +1,8 @@ +package team.aliens.dms.android.data.voting.model + +import java.util.UUID + +data class VotingItem( + val id: UUID, + val votingOptionName: String, +) diff --git a/data/src/dev/kotlin/team.aliens.dms.android.data/voting/repository/VotingRepository.kt b/data/src/dev/kotlin/team.aliens.dms.android.data/voting/repository/VotingRepository.kt new file mode 100644 index 000000000..c697017e5 --- /dev/null +++ b/data/src/dev/kotlin/team.aliens.dms.android.data/voting/repository/VotingRepository.kt @@ -0,0 +1,23 @@ +package team.aliens.dms.android.data.voting.repository + +import team.aliens.dms.android.data.student.model.Student +import java.time.LocalDate +import team.aliens.dms.android.data.voting.model.AllVoteSearch +import team.aliens.dms.android.data.voting.model.VotingItem +import java.util.UUID + +abstract class VotingRepository { + + abstract suspend fun fetchAllVoteSearch(): Result> + + abstract suspend fun fetchCheckVotingItem(votingTopicId: UUID): Result> + + abstract suspend fun fetchCreateVotingItem( + votingTopicId: UUID, + selectedId: UUID, + ): Result + + abstract suspend fun fetchDeleteVotingItem(voteId: UUID): Result + + abstract suspend fun fetchModelStudentCandidates(requestDate: LocalDate): Result> +} diff --git a/data/src/dev/kotlin/team.aliens.dms.android.data/voting/repository/VotingRepositoryImpl.kt b/data/src/dev/kotlin/team.aliens.dms.android.data/voting/repository/VotingRepositoryImpl.kt new file mode 100644 index 000000000..8eea55482 --- /dev/null +++ b/data/src/dev/kotlin/team.aliens.dms.android.data/voting/repository/VotingRepositoryImpl.kt @@ -0,0 +1,35 @@ +package team.aliens.dms.android.data.voting.repository + +import team.aliens.dms.android.core.network.exception.NotFoundException +import team.aliens.dms.android.data.student.model.Student +import java.time.LocalDate +import team.aliens.dms.android.data.voting.mapper.toModel +import team.aliens.dms.android.data.voting.model.AllVoteSearch +import team.aliens.dms.android.data.voting.model.VotingItem +import team.aliens.dms.android.network.voting.datasource.NetworkVotingDataSource +import java.util.UUID +import javax.inject.Inject + +internal class VotingRepositoryImpl @Inject constructor( + private val networkVotingDataSource: NetworkVotingDataSource, +) : VotingRepository() { + override suspend fun fetchAllVoteSearch(): Result> = runCatching { + networkVotingDataSource.fetchAllVoteSearch().toModel() + } + + override suspend fun fetchCheckVotingItem(votingTopicId: UUID): Result> = runCatching { + networkVotingDataSource.fetchCheckVotingItem(votingTopicId).toModel() + } + + override suspend fun fetchCreateVotingItem(votingTopicId: UUID, selectedId: UUID): Result = runCatching { + networkVotingDataSource.fetchCreateVotingItem(votingTopicId, selectedId).getOrThrow() + } + + override suspend fun fetchDeleteVotingItem(voteId: UUID): Result = runCatching { + networkVotingDataSource.fetchDeleteVotingItem(voteId) + } + + override suspend fun fetchModelStudentCandidates(requestDate: LocalDate): Result> = runCatching { + networkVotingDataSource.fetchModelStudentCandidates(requestDate).toModel() + } +} diff --git a/feature/build.gradle.kts b/feature/build.gradle.kts index 2f447ff0c..e486199b8 100644 --- a/feature/build.gradle.kts +++ b/feature/build.gradle.kts @@ -70,6 +70,8 @@ dependencies { implementation(project(ProjectPaths.Core.NOTIFICATION)) implementation(project(ProjectPaths.Core.JWT)) implementation(project(ProjectPaths.Core.ONBOARDING)) + implementation(project(ProjectPaths.Core.NETWORK)) + implementation(project(ProjectPaths.NETWORK)) implementation(project(ProjectPaths.DATA)) diff --git a/feature/src/dev/kotlin/team/aliens/dms/android/feature/main/application/navigation/ApplicationRoute.kt b/feature/src/dev/kotlin/team/aliens/dms/android/feature/main/application/navigation/ApplicationRoute.kt index 2eaef7773..501c8b75f 100644 --- a/feature/src/dev/kotlin/team/aliens/dms/android/feature/main/application/navigation/ApplicationRoute.kt +++ b/feature/src/dev/kotlin/team/aliens/dms/android/feature/main/application/navigation/ApplicationRoute.kt @@ -2,9 +2,23 @@ package team.aliens.dms.android.feature.main.application.navigation import androidx.compose.runtime.Composable import kotlinx.serialization.Serializable +import team.aliens.dms.android.core.designsystem.snackbar.DmsSnackBarType +import team.aliens.dms.android.data.voting.model.AllVoteSearch import team.aliens.dms.android.feature.main.application.ui.Application @Composable -fun ApplicationRoute() { - Application() +fun ApplicationRoute( + onNavigateRemainApplication: () -> Unit, + onNavigateOutingApplication: () -> Unit, + onNavigateVolunteerApplication: () -> Unit, + onNavigateVote: (AllVoteSearch) -> Unit, + onShowSnackBar: (DmsSnackBarType, String) -> Unit, +) { + Application( + onNavigateRemainApplication = onNavigateRemainApplication, + onNavigateOutingApplication = onNavigateOutingApplication, + onNavigateVolunteerApplication = onNavigateVolunteerApplication, + onNavigateVote = onNavigateVote, + onShowSnackBar = onShowSnackBar, + ) } diff --git a/feature/src/dev/kotlin/team/aliens/dms/android/feature/main/application/ui/ApplicationScreen.kt b/feature/src/dev/kotlin/team/aliens/dms/android/feature/main/application/ui/ApplicationScreen.kt index 9c1efbfdc..ef1d01cc2 100644 --- a/feature/src/dev/kotlin/team/aliens/dms/android/feature/main/application/ui/ApplicationScreen.kt +++ b/feature/src/dev/kotlin/team/aliens/dms/android/feature/main/application/ui/ApplicationScreen.kt @@ -1,13 +1,127 @@ package team.aliens.dms.android.feature.main.application.ui +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.launch +import team.aliens.dms.android.core.designsystem.DmsTheme +import team.aliens.dms.android.core.designsystem.snackbar.DmsSnackBarType +import team.aliens.dms.android.core.designsystem.tab.DmsTab +import team.aliens.dms.android.core.designsystem.tab.DmsTabRow +import team.aliens.dms.android.core.ui.navigation.LocalResultStore +import team.aliens.dms.android.data.voting.model.AllVoteSearch +import team.aliens.dms.android.feature.main.application.viewmodel.ApplicationState +import team.aliens.dms.android.feature.main.application.viewmodel.ApplicationViewModel +import team.aliens.dms.android.feature.main.application.ui.component.ApplicationContent +import team.aliens.dms.android.feature.main.application.ui.component.VoteContent @Composable -internal fun Application() { - ApplicationScreen() +internal fun Application( + onNavigateRemainApplication: () -> Unit, + onNavigateOutingApplication: () -> Unit, + onNavigateVolunteerApplication: () -> Unit, + onNavigateVote: (AllVoteSearch) -> Unit, + onShowSnackBar: (DmsSnackBarType, String) -> Unit, +) { + val viewModel: ApplicationViewModel = hiltViewModel() + val state by viewModel.uiState.collectAsStateWithLifecycle() + val resultStore = LocalResultStore.current + + LaunchedEffect(Unit) { + snapshotFlow { + resultStore.resultStateMap["remain_application_result"]?.value as? String? + }.collect { result -> + if (result != null) { + viewModel.setRemainApplication(result) + resultStore.removeResult(resultKey = "remain_application_result") + } + } + } + + ApplicationScreen( + state = state, + onNavigateRemainApplication = onNavigateRemainApplication, + onNavigateOutingApplication = { onShowSnackBar(DmsSnackBarType.SUCCESS, "준비중인 기능이에요") }, + onNavigateVolunteerApplication = { onShowSnackBar(DmsSnackBarType.SUCCESS, "준비중인 기능이에요") }, + onNavigateVote = onNavigateVote, + ) } @Composable -private fun ApplicationScreen() { - +private fun ApplicationScreen( + state: ApplicationState, + onNavigateRemainApplication: () -> Unit, + onNavigateOutingApplication: () -> Unit, + onNavigateVolunteerApplication: () -> Unit, + onNavigateVote: (AllVoteSearch) -> Unit, +) { + Column( + modifier = Modifier + .fillMaxSize() + .background(DmsTheme.colorScheme.background), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + val tabData = listOf( + "신청", + "투표", + ) + val pagerState = rememberPagerState( + pageCount = { tabData.size }, + initialPage = 0, + ) + val tabIndex = pagerState.currentPage + val coroutineScope = rememberCoroutineScope() + DmsTabRow( + selectedTabIndex = tabIndex, + ) { + tabData.forEachIndexed { index, text -> + DmsTab( + selected = tabIndex == index, + onClick = { + coroutineScope.launch { + pagerState.animateScrollToPage(index) + } + }, + text = text, + ) + } + } + HorizontalPager( + modifier = Modifier.fillMaxSize(), + state = pagerState, + beyondViewportPageCount = 1, + ) { page -> + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (page == 0) { + ApplicationContent( + appliedTitle = state.remainApplicationTitle, + onNavigateOutingApplication = onNavigateOutingApplication, + onNavigateRemainApplication = onNavigateRemainApplication, + onNavigateVolunteerApplication = onNavigateVolunteerApplication, + ) + } else { + VoteContent( + votes = state.votes, + onNavigateVote = onNavigateVote, + ) + } + } + } + } } diff --git a/feature/src/dev/kotlin/team/aliens/dms/android/feature/main/application/ui/component/ApplicationContent.kt b/feature/src/dev/kotlin/team/aliens/dms/android/feature/main/application/ui/component/ApplicationContent.kt new file mode 100644 index 000000000..c57114098 --- /dev/null +++ b/feature/src/dev/kotlin/team/aliens/dms/android/feature/main/application/ui/component/ApplicationContent.kt @@ -0,0 +1,43 @@ +package team.aliens.dms.android.feature.main.application.ui.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import team.aliens.dms.android.core.designsystem.R +import team.aliens.dms.android.core.designsystem.card.DmsApplicationCard + +@Composable +internal fun ApplicationContent( + modifier: Modifier = Modifier, + appliedTitle: String?, + onNavigateRemainApplication: () -> Unit, + onNavigateOutingApplication: () -> Unit, + onNavigateVolunteerApplication: () -> Unit, +) { + Column( + modifier = modifier + .fillMaxSize() + .padding( + horizontal = 10.dp, + vertical = 16.dp, + ), + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + listOf( + Triple("잔류", R.drawable.img_home, onNavigateRemainApplication), + Triple("외출 신청하기", R.drawable.img_outing, onNavigateOutingApplication), + Triple("봉사 활동 신청하기", R.drawable.img_volunteer, onNavigateVolunteerApplication), + ).forEach { (title, icon, onClick) -> + DmsApplicationCard( + title = title, + iconRes = icon, + onClick = onClick, + appliedTitle = if (title == "잔류") appliedTitle else null, + ) + } + } +} diff --git a/feature/src/dev/kotlin/team/aliens/dms/android/feature/main/application/ui/component/VoteContent.kt b/feature/src/dev/kotlin/team/aliens/dms/android/feature/main/application/ui/component/VoteContent.kt new file mode 100644 index 000000000..a10c1b50a --- /dev/null +++ b/feature/src/dev/kotlin/team/aliens/dms/android/feature/main/application/ui/component/VoteContent.kt @@ -0,0 +1,50 @@ +package team.aliens.dms.android.feature.main.application.ui.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import team.aliens.dms.android.core.designsystem.R +import team.aliens.dms.android.core.designsystem.card.DmsApplicationCard +import team.aliens.dms.android.core.ui.util.toDateString +import team.aliens.dms.android.data.voting.model.AllVoteSearch +import team.aliens.dms.android.data.voting.model.Vote +import team.aliens.dms.android.shared.date.toDate + +@Composable +internal fun VoteContent( + modifier: Modifier = Modifier, + votes: List, + onNavigateVote: (AllVoteSearch) -> Unit, +) { + LazyColumn( + modifier = modifier + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(20.dp), + contentPadding = PaddingValues( + horizontal = 10.dp, + vertical = 16.dp, + ), + ) { + items(votes) { vote -> + val icon = when (vote.voteType) { + Vote.STUDENT_VOTE -> R.drawable.img_student_tag + Vote.OPTION_VOTE -> R.drawable.img_percent + Vote.APPROVAL_VOTE -> R.drawable.img_choice + Vote.MODEL_STUDENT_VOTE -> R.drawable.img_model_student + } + DmsApplicationCard( + title = vote.topicName, + appliedTitle = null, + period = "${vote.startTime.toDateString()} ~ ${vote.endTime.toDateString()}", + description = vote.description, + iconRes = icon, + onClick = { onNavigateVote(vote) }, + ) + } + } +} diff --git a/feature/src/dev/kotlin/team/aliens/dms/android/feature/main/application/viewmodel/ApplicationViewModel.kt b/feature/src/dev/kotlin/team/aliens/dms/android/feature/main/application/viewmodel/ApplicationViewModel.kt new file mode 100644 index 000000000..bbed36381 --- /dev/null +++ b/feature/src/dev/kotlin/team/aliens/dms/android/feature/main/application/viewmodel/ApplicationViewModel.kt @@ -0,0 +1,60 @@ +package team.aliens.dms.android.feature.main.application.viewmodel + +import android.content.Context +import android.content.SharedPreferences +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import team.aliens.dms.android.core.ui.viewmodel.BaseStateViewModel +import team.aliens.dms.android.data.voting.model.AllVoteSearch +import team.aliens.dms.android.data.voting.repository.VotingRepository +import javax.inject.Inject + +@HiltViewModel +internal class ApplicationViewModel @Inject constructor( + private val votingRepository: VotingRepository, + @ApplicationContext private val context: Context, +) : BaseStateViewModel(ApplicationState()) { + + private val sharedPreferences: SharedPreferences = + context.getSharedPreferences("application_prefs", Context.MODE_PRIVATE) + + init { + getAllVotes() + loadAppliedRemainsTitle() + } + + private fun getAllVotes() { + viewModelScope.launch(Dispatchers.IO) { + votingRepository.fetchAllVoteSearch() + .onSuccess { votes -> + setState { it.copy(votes = votes) } + }.onFailure { +// Logger.a(it) { it.message.toString() } + } + } + } + + private fun loadAppliedRemainsTitle() { + val savedTitle = sharedPreferences.getString(KEY_APPLIED_REMAINS_TITLE, null) + if (savedTitle != null) { + setState { it.copy(remainApplicationTitle = savedTitle) } + } + } + + internal fun setRemainApplication(title: String) { + sharedPreferences.edit().putString(KEY_APPLIED_REMAINS_TITLE, title).apply() + setState { it.copy(remainApplicationTitle = title) } + } + + companion object { + private const val KEY_APPLIED_REMAINS_TITLE = "applied_remains_title" + } +} + +data class ApplicationState( + val votes: List = emptyList(), + val remainApplicationTitle: String? = null, +) diff --git a/feature/src/dev/kotlin/team/aliens/dms/android/feature/meal/ui/MealScreen.kt b/feature/src/dev/kotlin/team/aliens/dms/android/feature/meal/ui/MealScreen.kt index 574669365..0483dba13 100644 --- a/feature/src/dev/kotlin/team/aliens/dms/android/feature/meal/ui/MealScreen.kt +++ b/feature/src/dev/kotlin/team/aliens/dms/android/feature/meal/ui/MealScreen.kt @@ -14,6 +14,8 @@ import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.rememberModalBottomSheetState @@ -146,13 +148,15 @@ private fun MealScreen( Column( modifier = Modifier .fillMaxSize() - .background(DmsTheme.colorScheme.background), + .background(DmsTheme.colorScheme.background) + .verticalScroll(rememberScrollState()), ) { DmsTopAppBar( onBackPressed = onBackClick, ) Box( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize(), ) { Canvas( modifier = Modifier diff --git a/feature/src/dev/kotlin/team/aliens/dms/android/feature/remain/navigation/RemainApplicationRoute.kt b/feature/src/dev/kotlin/team/aliens/dms/android/feature/remain/navigation/RemainApplicationRoute.kt new file mode 100644 index 000000000..463d11170 --- /dev/null +++ b/feature/src/dev/kotlin/team/aliens/dms/android/feature/remain/navigation/RemainApplicationRoute.kt @@ -0,0 +1,16 @@ +package team.aliens.dms.android.feature.remain.navigation + +import androidx.compose.runtime.Composable +import team.aliens.dms.android.core.designsystem.snackbar.DmsSnackBarType +import team.aliens.dms.android.feature.remain.ui.RemainApplication + +@Composable +fun RemainApplicationRoute( + onNavigateBack: (String?) -> Unit, + onShowSnackBar: (DmsSnackBarType, String) -> Unit +) { + RemainApplication( + onNavigateBack = onNavigateBack, + onShowSnackBar = onShowSnackBar, + ) +} diff --git a/feature/src/dev/kotlin/team/aliens/dms/android/feature/remain/ui/RemainApplicationScreen.kt b/feature/src/dev/kotlin/team/aliens/dms/android/feature/remain/ui/RemainApplicationScreen.kt new file mode 100644 index 000000000..5403e3c4a --- /dev/null +++ b/feature/src/dev/kotlin/team/aliens/dms/android/feature/remain/ui/RemainApplicationScreen.kt @@ -0,0 +1,118 @@ +package team.aliens.dms.android.feature.remain.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import team.aliens.dms.android.core.designsystem.DmsTheme +import team.aliens.dms.android.core.designsystem.R +import team.aliens.dms.android.core.designsystem.appbar.DmsTopAppBar +import team.aliens.dms.android.core.designsystem.button.ButtonColor +import team.aliens.dms.android.core.designsystem.button.ButtonType +import team.aliens.dms.android.core.designsystem.button.DmsButton +import team.aliens.dms.android.core.designsystem.card.DmsApplicationCard +import team.aliens.dms.android.core.designsystem.horizontalPadding +import team.aliens.dms.android.core.designsystem.snackbar.DmsSnackBarType +import team.aliens.dms.android.core.designsystem.topPadding +import team.aliens.dms.android.core.designsystem.verticalPadding +import team.aliens.dms.android.core.ui.util.toLocale +import team.aliens.dms.android.feature.remain.ui.component.DmsFloatingNotice +import team.aliens.dms.android.feature.remain.viewmodel.RemainApplicationState +import team.aliens.dms.android.feature.remain.viewmodel.RemainApplicationViewModel +import java.util.UUID + +@Composable +internal fun RemainApplication( + onNavigateBack: (String?) -> Unit, + onShowSnackBar: (DmsSnackBarType, String) -> Unit, +) { + val remainApplicationViewModel: RemainApplicationViewModel = hiltViewModel() + val state by remainApplicationViewModel.uiState.collectAsStateWithLifecycle() + + RemainApplicationScreen( + onNavigateBack = onNavigateBack, + state = state, + setSelectRemainsOption = remainApplicationViewModel::setSelectRemainsOption, + changeRemainsOption = { + remainApplicationViewModel.changeRemainsOption(onShowSnackBar) + }, + ) +} + +@Composable +private fun RemainApplicationScreen( + onNavigateBack: (String?) -> Unit, + state: RemainApplicationState, + setSelectRemainsOption: (UUID?) -> Unit, + changeRemainsOption: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxSize() + .background(DmsTheme.colorScheme.background), + ) { + DmsTopAppBar( + title = "잔류 신청", + onBackPressed = { onNavigateBack(state.selectedRemainTitle) }, + ) + DmsFloatingNotice( + modifier = Modifier + .fillMaxWidth() + .padding( + top = 12.dp, + start = 24.dp, + end = 24.dp, + ), + text = "잔류 신청 시간은 ${state.remainsApplicationTime.startDayOfWeek.toLocale()} ${state.remainsApplicationTime.startTime} ~ ${state.remainsApplicationTime.endDayOfWeek.toLocale()} ${state.remainsApplicationTime.endTime} 까지 입니다.", + ) + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .topPadding(30.dp) + .horizontalPadding(24.dp), + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + items(state.remainsOptions) { remainsOption -> + val icon = when (remainsOption.title) { + "금요일 귀가" -> R.drawable.img_night_bus + "토요일 귀가" -> R.drawable.img_bus + "토요일 귀사" -> R.drawable.img_home + "주말 잔류" -> R.drawable.img_small_home + else -> R.drawable.img_bus + } + val appliedTitle = if (remainsOption.applied) "신청됨" else null + DmsApplicationCard( + title = remainsOption.title, + description = remainsOption.description, + isSelected = state.selectRemainsOptionId == remainsOption.id, + iconRes = icon, + appliedTitle = appliedTitle, + onClick = { setSelectRemainsOption(remainsOption.id) }, + ) + } + } + Spacer(modifier = Modifier.weight(1f)) + DmsButton( + modifier = Modifier + .fillMaxWidth() + .horizontalPadding(24.dp) + .verticalPadding(16.dp), + text = "변경하기", + buttonType = ButtonType.Contained, + buttonColor = ButtonColor.Primary, + enabled = state.selectRemainsOptionId != null, + onClick = changeRemainsOption, + ) + } +} diff --git a/feature/src/dev/kotlin/team/aliens/dms/android/feature/remain/ui/component/DmsFloatingNotice.kt b/feature/src/dev/kotlin/team/aliens/dms/android/feature/remain/ui/component/DmsFloatingNotice.kt new file mode 100644 index 000000000..5d95bfa8c --- /dev/null +++ b/feature/src/dev/kotlin/team/aliens/dms/android/feature/remain/ui/component/DmsFloatingNotice.kt @@ -0,0 +1,65 @@ +package team.aliens.dms.android.feature.remain.ui.component + +import android.R +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.text.TextAutoSize +import androidx.compose.foundation.text.TextAutoSizeDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.widget.TextViewCompat +import team.aliens.dms.android.core.designsystem.DmsTheme +import team.aliens.dms.android.core.designsystem.foundation.DmsIcon +import team.aliens.dms.android.core.designsystem.labelM + +@Composable +fun DmsFloatingNotice( + modifier: Modifier = Modifier, + text: String, + @DrawableRes iconResource: Int = DmsIcon.Notification, +) { + + Row( + modifier = modifier + .fillMaxWidth() + .background( + color = DmsTheme.colorScheme.primary, + shape = RoundedCornerShape(30.dp), + ).padding( + horizontal = 22.dp, + vertical = 12.dp, + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp), + ) { + Icon( + painter = painterResource(iconResource), + contentDescription = null, + ) + BasicText( + text = text, + style = DmsTheme.typography.labelM.copy( + color = DmsTheme.colorScheme.onBackground, + ), + autoSize = TextAutoSize.StepBased( + minFontSize = 1.sp, + maxFontSize = 50.sp, + ), + maxLines = 1, + ) + } +} diff --git a/feature/src/dev/kotlin/team/aliens/dms/android/feature/remain/viewmodel/RemainApplicationViewModel.kt b/feature/src/dev/kotlin/team/aliens/dms/android/feature/remain/viewmodel/RemainApplicationViewModel.kt new file mode 100644 index 000000000..b664dab16 --- /dev/null +++ b/feature/src/dev/kotlin/team/aliens/dms/android/feature/remain/viewmodel/RemainApplicationViewModel.kt @@ -0,0 +1,117 @@ +package team.aliens.dms.android.feature.remain.viewmodel + +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import team.aliens.dms.android.core.designsystem.snackbar.DmsSnackBarType +import team.aliens.dms.android.core.ui.viewmodel.BaseStateViewModel +import team.aliens.dms.android.data.remain.model.RemainsApplicationTime +import team.aliens.dms.android.data.remain.model.RemainsOption +import team.aliens.dms.android.data.remain.repository.RemainsRepository +import java.time.LocalDateTime +import java.time.LocalTime +import java.util.UUID +import javax.inject.Inject + +@HiltViewModel +class RemainApplicationViewModel @Inject constructor( + val remainsRepository: RemainsRepository +): BaseStateViewModel(RemainApplicationState()) { + + init { + getRemainsOptions() + getRemainsApplicationTime() + } + + private fun getRemainsOptions() { + viewModelScope.launch { + remainsRepository.fetchRemainsOptions().onSuccess { remainsOptions -> + val selectRemainsOptionId = + remainsOptions.find { it.applied }?.id ?: UUID.randomUUID() + setState { + it.copy( + remainsOptions = remainsOptions, + selectRemainsOptionId = selectRemainsOptionId, + ) + } + } + } + } + + private fun getRemainsApplicationTime() { + viewModelScope.launch { + remainsRepository.fetchRemainsApplicationTime().onSuccess { time -> + setState { it.copy(remainsApplicationTime = time) } + } + } + } + + internal fun setSelectRemainsOption(remainsOptionId: UUID?) { + setState { it.copy(selectRemainsOptionId = remainsOptionId) } + } + + internal fun changeRemainsOption( + onShowSnackBar: (DmsSnackBarType, String) -> Unit, + ) { + viewModelScope.launch { + if (!isWithinApplicationTime()) { + onShowSnackBar(DmsSnackBarType.ERROR, "잔류 신청 시간이 아닙니다") + return@launch + } + uiState.value.selectRemainsOptionId?.let { optionId -> + remainsRepository.updateRemainsOption(optionId = optionId) + .onSuccess { + val remainsOptions = uiState.value.remainsOptions.map { remainsOption -> + remainsOption.copy(applied = remainsOption.id == uiState.value.selectRemainsOptionId) + } + val appliedOption = remainsOptions.find { it.applied } + setState { + it.copy( + remainsOptions = remainsOptions, + selectedRemainTitle = appliedOption?.title + ) + } + onShowSnackBar(DmsSnackBarType.SUCCESS, "잔류 신청이 완료되었습니다") + }.onFailure { + onShowSnackBar(DmsSnackBarType.ERROR, "잔류 신청에 실패했습니다") + } + } + } + } + + private fun isWithinApplicationTime(): Boolean { + val now = LocalDateTime.now() + val currentDayOfWeek = now.dayOfWeek.value + val currentTime = now.toLocalTime() + + val applicationTime = uiState.value.remainsApplicationTime + + if (applicationTime.startTime.isEmpty() || applicationTime.endTime.isEmpty()) { + return true + } + + val startTime = LocalTime.parse(applicationTime.startTime) + val endTime = LocalTime.parse(applicationTime.endTime) + val startDayValue = applicationTime.startDayOfWeek.value + val endDayValue = applicationTime.endDayOfWeek.value + + if (startDayValue == endDayValue) { + return currentDayOfWeek == startDayValue && + currentTime >= startTime && currentTime <= endTime + } + + return when (currentDayOfWeek) { + startDayValue -> currentTime >= startTime + endDayValue -> currentTime <= endTime + in (startDayValue + 1).. true + else -> false + } + } +} + +data class RemainApplicationState( + val remainsOptions: List = emptyList(), + val selectRemainsOptionId: UUID? = null, + val selectedRemainTitle: String? = null, + val remainsApplicationTime: RemainsApplicationTime = RemainsApplicationTime(), +) diff --git a/feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/navigation/VoteRoute.kt b/feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/navigation/VoteRoute.kt new file mode 100644 index 000000000..9918b445c --- /dev/null +++ b/feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/navigation/VoteRoute.kt @@ -0,0 +1,23 @@ +package team.aliens.dms.android.feature.vote.navigation + +import androidx.compose.runtime.Composable +import team.aliens.dms.android.core.designsystem.snackbar.DmsSnackBarType +import team.aliens.dms.android.feature.vote.ui.Vote +import java.time.LocalDateTime + +@Composable +fun VoteRoute( + title: String, + startTime: String, + endTime: String, + onShowSnackBar: (DmsSnackBarType, String) -> Unit, + onNavigateBack: () -> Unit, +) { + Vote( + title = title, + startTime = startTime, + endTime = endTime, + onShowSnackBar = onShowSnackBar, + onNavigateBack = onNavigateBack, + ) +} diff --git a/feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/ui/VoteScreen.kt b/feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/ui/VoteScreen.kt new file mode 100644 index 000000000..a8d9e2625 --- /dev/null +++ b/feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/ui/VoteScreen.kt @@ -0,0 +1,120 @@ +package team.aliens.dms.android.feature.vote.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import team.aliens.dms.android.core.designsystem.DmsTheme +import team.aliens.dms.android.core.designsystem.appbar.DmsTopAppBar +import team.aliens.dms.android.core.designsystem.button.ButtonColor +import team.aliens.dms.android.core.designsystem.button.ButtonType +import team.aliens.dms.android.core.designsystem.button.DmsLayeredButton +import team.aliens.dms.android.core.designsystem.snackbar.DmsSnackBarType +import team.aliens.dms.android.feature.vote.ui.component.VoteItemContent +import team.aliens.dms.android.feature.vote.viewmodel.VoteSideEffect +import team.aliens.dms.android.feature.vote.viewmodel.VoteState +import team.aliens.dms.android.feature.vote.viewmodel.VoteViewModel +import java.util.UUID + +@Composable +internal fun Vote( + title: String, + startTime: String, + endTime: String, + onShowSnackBar: (DmsSnackBarType, String) -> Unit, + onNavigateBack: () -> Unit, +) { + val viewModel: VoteViewModel = hiltViewModel() + val state by viewModel.uiState.collectAsStateWithLifecycle() + + LaunchedEffect(Unit) { + viewModel.initState(title, startTime, endTime) + viewModel.sideEffect.collect { + when (it) { + is VoteSideEffect.VoteSuccess -> onShowSnackBar( + DmsSnackBarType.SUCCESS, + "투표를 완료했어요!", + ) + + is VoteSideEffect.VoteConflict -> onShowSnackBar( + DmsSnackBarType.ERROR, + "이미 해당 투표에 참여했어요", + ) + + is VoteSideEffect.VoteFail -> onShowSnackBar( + DmsSnackBarType.ERROR, + "투표 중 오류가 발생했어요", + ) + + is VoteSideEffect.VoteLoadFail -> onShowSnackBar( + DmsSnackBarType.ERROR, + "정보를 불러오지 못했어요", + ) + } + } + } + + VoteScreen( + state = state, + onNavigateBack = onNavigateBack, + onSelectItem = { selectedId -> viewModel.setSelectId(UUID.fromString(selectedId)) }, + submitVote = viewModel::postVote, + ) +} + +@Composable +private fun VoteScreen( + modifier: Modifier = Modifier, + state: VoteState, + onNavigateBack: () -> Unit, + onSelectItem: (String) -> Unit, + submitVote: () -> Unit, +) { + Column( + modifier = modifier + .fillMaxSize() + .background(DmsTheme.colorScheme.background), + ) { + DmsTopAppBar( + onBackPressed = onNavigateBack, + ) + VoteItemContent( + modifier = Modifier + .fillMaxSize() + .weight(1f), + vote = state.vote.voteType, + title = state.vote.topicName, + startTime = state.vote.startTime, + endTime = state.vote.endTime, + options = state.options, + students = state.students, + modelStudents = state.modelStudent, + selectItem = state.selectId.toString(), + onSelect = onSelectItem, + ) + DmsLayeredButton( + modifier = Modifier + .fillMaxWidth(), + text = "투표하기", + buttonType = ButtonType.Contained, + buttonColor = ButtonColor.Primary, + shape = RoundedCornerShape( + topStart = 32.dp, + topEnd = 32.dp, + bottomStart = 0.dp, + bottomEnd = 0.dp, + ), + onClick = submitVote, + enabled = state.buttonEnabled, + isLoading = state.isLoading, + ) + } +} diff --git a/feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/ui/component/ApprovalContent.kt b/feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/ui/component/ApprovalContent.kt new file mode 100644 index 000000000..69c18590c --- /dev/null +++ b/feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/ui/component/ApprovalContent.kt @@ -0,0 +1,151 @@ + +package team.aliens.dms.android.feature.vote.ui.component + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +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.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import team.aliens.dms.android.core.designsystem.DmsTheme +import team.aliens.dms.android.core.designsystem.R +import team.aliens.dms.android.core.designsystem.bodyM +import team.aliens.dms.android.core.designsystem.modifier.DmsShadowType +import team.aliens.dms.android.core.designsystem.modifier.dmsShadowModifier +import team.aliens.dms.android.core.designsystem.titleB +import team.aliens.dms.android.core.designsystem.util.clickable +import team.aliens.dms.android.data.voting.model.VotingItem + +@Composable +internal fun ApprovalContent( + modifier: Modifier = Modifier, + title: String, + options: List, + selectItem: String, + onSelect: (String) -> Unit, +) { + Column( + modifier = modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy( + space = 60.dp, + alignment = Alignment.CenterVertically, + ), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp), + text = title, + style = DmsTheme.typography.titleB, + color = DmsTheme.colorScheme.surfaceContainer, + textAlign = TextAlign.Center, + ) + Row( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(0.4f) + .padding(horizontal = 24.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + options.forEachIndexed { index, option -> + if (index == 0) { + ApprovalItem( + modifier = Modifier.weight(1f), + imageResource = R.drawable.ic_approve, + isSelected = option.id.toString() == selectItem, + clickColor = DmsTheme.colorScheme.onPrimary, + clickBorderColor = DmsTheme.colorScheme.onPrimaryContainer, + title = option.votingOptionName, + contentColor = DmsTheme.colorScheme.onPrimaryContainer, + clickContentColor = DmsTheme.colorScheme.inversePrimary, + onClick = { onSelect(option.id.toString()) }, + ) + } else { + ApprovalItem( + modifier = Modifier.weight(1f), + imageResource = R.drawable.ic_oppose, + isSelected = option.id.toString() == selectItem, + clickColor = DmsTheme.colorScheme.onError, + clickBorderColor = DmsTheme.colorScheme.onErrorContainer, + title = option.votingOptionName, + contentColor = DmsTheme.colorScheme.onErrorContainer, + clickContentColor = DmsTheme.colorScheme.outline, + onClick = { onSelect(option.id.toString()) }, + ) + } + } + } + } +} + +@Composable +private fun ApprovalItem( + modifier: Modifier = Modifier, + @DrawableRes imageResource: Int, + isSelected: Boolean, + clickColor: Color, + clickBorderColor: Color, + contentColor: Color, + clickContentColor: Color, + title: String, + onClick: () -> Unit, +) { + val (backgroundColor, borderColor, content) = if (isSelected) { + Triple(clickColor, clickBorderColor, clickContentColor) + } else { + Triple(DmsTheme.colorScheme.surfaceTint, DmsTheme.colorScheme.surfaceVariant, contentColor) + } + Card( + modifier = modifier + .dmsShadowModifier( + dmsShadowType = DmsShadowType.Light20, + shape = RoundedCornerShape(12.dp), + ) + .clickable(onClick = onClick), + colors = CardDefaults.cardColors( + containerColor = backgroundColor, + ), + shape = RoundedCornerShape(32.dp), + border = BorderStroke( + width = 2.dp, + color = borderColor, + ), + ) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy( + space = 20.dp, + alignment = Alignment.CenterVertically, + ), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + painter = painterResource(imageResource), + tint = content, + contentDescription = null, + ) + Text( + text = title, + style = DmsTheme.typography.bodyM, + color = content, + ) + } + } +} diff --git a/feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/ui/component/OptionContent.kt b/feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/ui/component/OptionContent.kt new file mode 100644 index 000000000..3109cfb6f --- /dev/null +++ b/feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/ui/component/OptionContent.kt @@ -0,0 +1,101 @@ +package team.aliens.dms.android.feature.vote.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.HorizontalDivider +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.unit.dp +import java.time.LocalDateTime +import team.aliens.dms.android.core.designsystem.DmsTheme +import team.aliens.dms.android.core.designsystem.bodyB +import team.aliens.dms.android.core.designsystem.foundation.DmsIcon +import team.aliens.dms.android.core.designsystem.startPadding +import team.aliens.dms.android.core.designsystem.util.clickable +import team.aliens.dms.android.data.voting.model.VotingItem + +@Composable +internal fun OptionContent( + modifier: Modifier = Modifier, + title: String, + startTime: LocalDateTime, + endTime: LocalDateTime, + options: List, + selectItem: String, + onSelect: (String) -> Unit, +) { + Column( + modifier = modifier, + ) { + TitleContent( + title = title, + startTime = startTime, + endTime = endTime, + ) + HorizontalDivider( + modifier = Modifier.fillMaxWidth(), + thickness = 0.4.dp, + color = DmsTheme.colorScheme.onSurfaceVariant, + ) + LazyColumn { + items( + items = options, + key = { it.id }, + ) { option -> + OptionItem( + title = option.votingOptionName, + selected = option.id.toString() == selectItem, + onClick = { onSelect(option.id.toString()) }, + ) + } + } + } +} + +@Composable +private fun OptionItem( + modifier: Modifier = Modifier, + title: String, + selected: Boolean, + onClick: () -> Unit, +) { + val backgroundColor = if (selected) { + DmsTheme.colorScheme.surfaceVariant + } else { + DmsTheme.colorScheme.background + } + Row( + modifier = modifier + .fillMaxWidth() + .background(backgroundColor) + .clickable(onClick = onClick) + .padding( + horizontal = 24.dp, + vertical = 18.dp, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.startPadding(12.dp), + text = title, + style = DmsTheme.typography.bodyB, + color = DmsTheme.colorScheme.inverseOnSurface, + ) + Spacer(modifier = Modifier.weight(1f)) + Icon( + painter = painterResource(DmsIcon.Forward), + tint = DmsTheme.colorScheme.scrim, + contentDescription = null, + ) + } +} diff --git a/feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/ui/component/StudentContent.kt b/feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/ui/component/StudentContent.kt new file mode 100644 index 000000000..5b58317cc --- /dev/null +++ b/feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/ui/component/StudentContent.kt @@ -0,0 +1,160 @@ +package team.aliens.dms.android.feature.vote.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import kotlinx.coroutines.launch +import team.aliens.dms.android.core.designsystem.DmsTheme +import team.aliens.dms.android.core.designsystem.bodyB +import team.aliens.dms.android.core.designsystem.foundation.DmsIcon +import team.aliens.dms.android.core.designsystem.startPadding +import team.aliens.dms.android.core.designsystem.tab.DmsTab +import team.aliens.dms.android.core.designsystem.tab.DmsTabRow +import team.aliens.dms.android.core.designsystem.util.clickable +import team.aliens.dms.android.data.student.model.Student +import java.time.LocalDateTime + +@Composable +internal fun StudentContent( + modifier: Modifier = Modifier, + title: String, + startTime: LocalDateTime, + endTime: LocalDateTime, + students: List, + selectItem: String, + onSelect: (String) -> Unit, +) { + val grades = listOf("1학년", "2학년", "3학년") + val pagerState = rememberPagerState( + pageCount = { grades.size }, + initialPage = 0, + ) + val tabIndex = pagerState.currentPage + val coroutineScope = rememberCoroutineScope() + + Column( + modifier = modifier.fillMaxSize(), + ) { + TitleContent( + title = title, + startTime = startTime, + endTime = endTime, + ) + HorizontalDivider( + modifier = Modifier.fillMaxWidth(), + thickness = 0.4.dp, + color = DmsTheme.colorScheme.onSurfaceVariant, + ) + DmsTabRow( + selectedTabIndex = tabIndex, + ) { + grades.forEachIndexed { index, text -> + DmsTab( + selected = tabIndex == index, + onClick = { + coroutineScope.launch { + pagerState.animateScrollToPage(index) + } + }, + text = text, + ) + } + } + HorizontalPager( + modifier = Modifier.fillMaxWidth(), + state = pagerState, + beyondViewportPageCount = 1, + ) { page -> + val selectGrade = page + 1 + LazyColumn(modifier = Modifier.fillMaxSize()) { + val filteredStudents = students.filter { it.gradeClassNumber.startsWith("$selectGrade") } + items( + items = filteredStudents, + key = { student -> student.id }, + ) { student -> + StudentItem( + profileImageUrl = student.profileImageUrl, + name = student.name, + gcn = student.gradeClassNumber, + isSelected = selectItem == student.id.toString(), + onClick = { onSelect(student.id.toString()) }, + ) + } + } + } + } +} + +@Composable +private fun StudentItem( + modifier: Modifier = Modifier, + profileImageUrl: String, + name: String, + gcn: String, + isSelected: Boolean, + onClick: () -> Unit, +) { + val backgroundColor = if (isSelected) { + DmsTheme.colorScheme.surfaceVariant + } else { + DmsTheme.colorScheme.background + } + Row( + modifier = modifier + .fillMaxWidth() + .background(backgroundColor) + .clickable(onClick = onClick) + .padding( + horizontal = 24.dp, + vertical = 18.dp, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + AsyncImage( + modifier = Modifier + .size(32.dp) + .clip(CircleShape), + model = ImageRequest.Builder(context = LocalContext.current) + .data(profileImageUrl) + .build(), + contentDescription = null, + contentScale = ContentScale.Crop, + ) + Text( + modifier = Modifier.startPadding(12.dp), + text = "$gcn $name", + style = DmsTheme.typography.bodyB, + color = DmsTheme.colorScheme.inverseOnSurface, + ) + Spacer(modifier = Modifier.weight(1f)) + Icon( + painter = painterResource(DmsIcon.Forward), + tint = DmsTheme.colorScheme.scrim, + contentDescription = null, + ) + } +} diff --git a/feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/ui/component/TitleContent.kt b/feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/ui/component/TitleContent.kt new file mode 100644 index 000000000..5534f94db --- /dev/null +++ b/feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/ui/component/TitleContent.kt @@ -0,0 +1,41 @@ +package team.aliens.dms.android.feature.vote.ui.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.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import java.time.LocalDateTime +import team.aliens.dms.android.core.designsystem.DmsTheme +import team.aliens.dms.android.core.designsystem.lBodyB +import team.aliens.dms.android.core.designsystem.labelM +import team.aliens.dms.android.core.ui.util.toDateString + +@Composable +internal fun TitleContent( + modifier: Modifier = Modifier, + title: String, + startTime: LocalDateTime, + endTime: LocalDateTime, +) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = title, + style = DmsTheme.typography.lBodyB, + color = DmsTheme.colorScheme.tertiaryContainer, + ) + Text( + text = "${startTime.toDateString()} ~ ${endTime.toDateString()}", + style = DmsTheme.typography.labelM, + color = DmsTheme.colorScheme.inverseOnSurface, + ) + } +} diff --git a/feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/ui/component/VoteItemContent.kt b/feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/ui/component/VoteItemContent.kt new file mode 100644 index 000000000..7b3b7aa02 --- /dev/null +++ b/feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/ui/component/VoteItemContent.kt @@ -0,0 +1,71 @@ +package team.aliens.dms.android.feature.vote.ui.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import team.aliens.dms.android.data.student.model.Student +import team.aliens.dms.android.data.voting.model.Vote +import team.aliens.dms.android.data.voting.model.VotingItem +import java.time.LocalDateTime + +@Composable +internal fun VoteItemContent( + modifier: Modifier = Modifier, + vote: Vote, + title: String, + startTime: LocalDateTime, + endTime: LocalDateTime, + options: List, + students: List, + modelStudents: List, + selectItem: String, + onSelect: (String) -> Unit, +) { + Column( + modifier = modifier, + ) { + when (vote) { + Vote.OPTION_VOTE -> { + OptionContent( + title = title, + startTime = startTime, + endTime = endTime, + options = options, + selectItem = selectItem, + onSelect = onSelect, + ) + } + + Vote.STUDENT_VOTE -> { + StudentContent( + title = title, + startTime = startTime, + endTime = endTime, + students = students, + selectItem = selectItem, + onSelect = onSelect, + ) + } + + Vote.APPROVAL_VOTE -> { + ApprovalContent( + title = title, + options = options, + selectItem = selectItem, + onSelect = onSelect, + ) + } + + Vote.MODEL_STUDENT_VOTE -> { + StudentContent( + title = title, + startTime = startTime, + endTime = endTime, + students = modelStudents, + selectItem = selectItem, + onSelect = onSelect, + ) + } + } + } +} diff --git a/feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/viewmodel/VoteViewModel.kt b/feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/viewmodel/VoteViewModel.kt new file mode 100644 index 000000000..439dc53ce --- /dev/null +++ b/feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/viewmodel/VoteViewModel.kt @@ -0,0 +1,124 @@ +package team.aliens.dms.android.feature.vote.viewmodel + +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import team.aliens.dms.android.core.network.exception.ConflictException +import team.aliens.dms.android.core.ui.viewmodel.BaseStateViewModel +import team.aliens.dms.android.data.student.model.Student +import team.aliens.dms.android.data.student.repository.StudentRepository +import team.aliens.dms.android.data.voting.model.AllVoteSearch +import team.aliens.dms.android.data.voting.model.Vote +import team.aliens.dms.android.data.voting.model.VotingItem +import team.aliens.dms.android.data.voting.repository.VotingRepository +import team.aliens.dms.android.shared.date.toLocalDateTime +import team.aliens.dms.android.shared.date.util.today +import java.util.UUID +import javax.inject.Inject + +@HiltViewModel +internal class VoteViewModel @Inject constructor( + private val voteRepository: VotingRepository, + private val studentRepository: StudentRepository, +) : BaseStateViewModel(VoteState()) { + + init { + fetchVotesByType() + } + + internal fun initState(title: String, startTime: String, endTime: String) { + setState { + it.copy( + vote = it.vote.copy(topicName = title, startTime = startTime.toLocalDateTime(), endTime = endTime.toLocalDateTime()), + ) + } + } + + private fun fetchVotesByType() { + when (uiState.value.vote.voteType) { + Vote.OPTION_VOTE -> getVoteItems() + Vote.STUDENT_VOTE -> getStudents() + Vote.APPROVAL_VOTE -> getVoteItems() + Vote.MODEL_STUDENT_VOTE -> getCandidateModelStudents() + } + } + + private fun getStudents() { + viewModelScope.launch(Dispatchers.IO) { + studentRepository.fetchStudents() + .onSuccess { student -> setState { it.copy(students = student) } } + .onFailure { + sendEffect(VoteSideEffect.VoteLoadFail) + } + } + } + + private fun getVoteItems() { + viewModelScope.launch(Dispatchers.IO) { + voteRepository.fetchCheckVotingItem(uiState.value.vote.id) + .onSuccess { voteItems -> setState { it.copy(options = voteItems) } } + .onFailure { + sendEffect(VoteSideEffect.VoteLoadFail) + } + } + } + + private fun getCandidateModelStudents() { + viewModelScope.launch(Dispatchers.IO) { + voteRepository.fetchModelStudentCandidates(requestDate = today) + .onSuccess { modelStudents -> setState { it.copy(modelStudent = modelStudents) } } + .onFailure { + sendEffect(VoteSideEffect.VoteLoadFail) + } + } + } + + internal fun setSelectId(selectId: UUID) { + setState { it.copy(selectId = selectId) } + setButtonEnabled() + } + + private fun setButtonEnabled() { + val isSelectIdNotNull = uiState.value.selectId != null + setState { uiState.value.copy(buttonEnabled = isSelectIdNotNull) } + } + + internal fun postVote() { + with(uiState.value) { + viewModelScope.launch(Dispatchers.IO) { + setState { uiState.value.copy(isLoading = true, buttonEnabled = false) } + voteRepository.fetchCreateVotingItem( + votingTopicId = uiState.value.vote.id, + selectedId = uiState.value.selectId ?: UUID.randomUUID(), + ).onSuccess { + setState { uiState.value.copy(buttonEnabled = false, isLoading = false) } + sendEffect(VoteSideEffect.VoteSuccess) + }.onFailure { + setState { uiState.value.copy(isLoading = false, buttonEnabled = true) } + when (it) { + is ConflictException -> sendEffect(VoteSideEffect.VoteConflict) + else -> sendEffect(VoteSideEffect.VoteFail) + } + } + } + } + } +} + +internal data class VoteState( + val vote: AllVoteSearch = AllVoteSearch(), + val options: List = emptyList(), + val students: List = emptyList(), + val modelStudent: List = emptyList(), + val selectId: UUID? = null, + val buttonEnabled: Boolean = false, + val isLoading: Boolean = false, +) + +internal sealed interface VoteSideEffect { + data object VoteSuccess : VoteSideEffect + data object VoteConflict : VoteSideEffect + data object VoteFail : VoteSideEffect + data object VoteLoadFail : VoteSideEffect +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5a919dfb2..c4f2577dd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -39,7 +39,7 @@ navigation3 = "1.0.0" hiltNavigation = "1.1.0" coil = "2.4.0" compileSdk = "36" -minSdk = "24" +minSdk = "26" targetSdk = "34" desugarJdkLibs = "2.0.3" java = "17" diff --git a/network/src/dev/kotlin/team.aliens.dms.android.network/voting/apiservice/VotingApiService.kt b/network/src/dev/kotlin/team.aliens.dms.android.network/voting/apiservice/VotingApiService.kt index eac7cc4a9..eeea69044 100644 --- a/network/src/dev/kotlin/team.aliens.dms.android.network/voting/apiservice/VotingApiService.kt +++ b/network/src/dev/kotlin/team.aliens.dms.android.network/voting/apiservice/VotingApiService.kt @@ -30,7 +30,7 @@ internal interface VotingApiService { suspend fun fetchCreateVotingItem( @Path("voting-topic-id") votingTopicId: UUID, @Query("selected-id") selectedId: UUID, - ): Response? + ): Result? @DELETE("/votes/student/{vote_id}") @RequiresAccessToken diff --git a/network/src/dev/kotlin/team.aliens.dms.android.network/voting/datasource/NetworkVotingDataSource.kt b/network/src/dev/kotlin/team.aliens.dms.android.network/voting/datasource/NetworkVotingDataSource.kt index 3390a7c7a..9b2150331 100644 --- a/network/src/dev/kotlin/team.aliens.dms.android.network/voting/datasource/NetworkVotingDataSource.kt +++ b/network/src/dev/kotlin/team.aliens.dms.android.network/voting/datasource/NetworkVotingDataSource.kt @@ -15,7 +15,7 @@ abstract class NetworkVotingDataSource { abstract suspend fun fetchCreateVotingItem( votingTopicId: UUID, selectedId: UUID, - ) + ): Result abstract suspend fun fetchDeleteVotingItem(voteId: UUID) diff --git a/network/src/dev/kotlin/team.aliens.dms.android.network/voting/datasource/NetworkVotingDataSourceImpl.kt b/network/src/dev/kotlin/team.aliens.dms.android.network/voting/datasource/NetworkVotingDataSourceImpl.kt index 567861f2a..74e71f68f 100644 --- a/network/src/dev/kotlin/team.aliens.dms.android.network/voting/datasource/NetworkVotingDataSourceImpl.kt +++ b/network/src/dev/kotlin/team.aliens.dms.android.network/voting/datasource/NetworkVotingDataSourceImpl.kt @@ -18,8 +18,9 @@ internal class NetworkVotingDataSourceImpl @Inject constructor( override suspend fun fetchCheckVotingItem(votingTopicId: UUID): FetchCheckVotingItemResponse = handleNetworkRequest { votingApiService.fetchCheckVotingItem(votingTopicId) } - override suspend fun fetchCreateVotingItem(votingTopicId: UUID, selectedId: UUID): Unit = + override suspend fun fetchCreateVotingItem(votingTopicId: UUID, selectedId: UUID): Result = runCatching { handleNetworkRequest { votingApiService.fetchCreateVotingItem(votingTopicId, selectedId) } + } override suspend fun fetchDeleteVotingItem(voteId: UUID): Unit = handleNetworkRequest { votingApiService.fetchDeleteVotingItem(voteId) }