From 6085790f3c8335d6248464c439490db2003ea84b Mon Sep 17 00:00:00 2001 From: jinukeu Date: Thu, 28 Dec 2023 17:36:17 +0900 Subject: [PATCH 01/14] =?UTF-8?q?feat/#75:=20SuwikiTabBar=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/tabbar/SuwikiTabBar.kt | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 core/designsystem/src/main/java/com/suwiki/core/designsystem/component/tabbar/SuwikiTabBar.kt diff --git a/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/tabbar/SuwikiTabBar.kt b/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/tabbar/SuwikiTabBar.kt new file mode 100644 index 000000000..981daefce --- /dev/null +++ b/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/tabbar/SuwikiTabBar.kt @@ -0,0 +1,172 @@ +package com.suwiki.core.designsystem.component.tabbar + +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.layout.SubcomposeLayout +import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.suwiki.core.designsystem.theme.Black +import com.suwiki.core.designsystem.theme.Gray95 +import com.suwiki.core.designsystem.theme.SuwikiTheme +import com.suwiki.core.designsystem.theme.White +import com.suwiki.core.ui.extension.suwikiClickable + +enum class SubComposeID { + PRE_CALCULATE_ITEM, + ITEM, + INDICATOR +} + +data class TabPosition( + val left: Dp, + val width: Dp, +) + +// https://github.com/GautierLouis/ComposePlayground/blob/1c5ffb483291bb21d9820a2fe7a18aec049c7bd2/app/src/main/java/com/louis/composeplayground/ui/TabRow.kt +@Composable +fun SuwikiTabBar( + indicatorColor: Color = Black, + paddingValues: PaddingValues = PaddingValues(start = 24.dp, end = 12.dp), + animationSpec: AnimationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing), + selectedTabPosition: Int = 0, + tabItem: @Composable () -> Unit, +) { + Surface( + color = White, + ) { + SubcomposeLayout( + Modifier + .padding(paddingValues) + .selectableGroup(), + ) { constraints -> + val tabMeasurable: List = subcompose(SubComposeID.PRE_CALCULATE_ITEM, tabItem) + .map { it.measure(constraints) } + + val maxItemHeight = tabMeasurable.maxOf { it.height } + + val tabPlacables = subcompose(SubComposeID.ITEM, tabItem).map { + it.measure(constraints) + } + + val tabPositions = tabPlacables.mapIndexed { index, placeable -> + val itemWidth = placeable.width + val x = tabPlacables.take(index).sumOf { it.width } + TabPosition(x.toDp(), itemWidth.toDp()) + } + + val tabRowWidth = tabPlacables.sumOf { it.width } + + layout(tabRowWidth, maxItemHeight) { + subcompose(SubComposeID.INDICATOR) { + Box( + Modifier + .tabIndicator(tabPositions[selectedTabPosition], animationSpec) + .fillMaxWidth() + .height(maxItemHeight.toDp()) + .background(color = indicatorColor), + ) + }.forEach { + it.measure(Constraints.fixed(tabRowWidth, maxItemHeight)).placeRelative(0, 0) + } + + tabPlacables.forEachIndexed { index, placeable -> + val x = tabPlacables.take(index).sumOf { it.width } + placeable.placeRelative(x, 0) + } + } + } + } +} + +private fun Modifier.tabIndicator( + tabPosition: TabPosition, + animationSpec: AnimationSpec, +): Modifier = composed( + inspectorInfo = debugInspectorInfo { + name = "tabIndicatorOffset" + value = tabPosition + }, +) { + val currentTabWidth by animateDpAsState( + targetValue = tabPosition.width, + animationSpec = animationSpec, label = "", + ) + val indicatorOffset by animateDpAsState( + targetValue = tabPosition.left, + animationSpec = animationSpec, label = "", + ) + fillMaxWidth() + .wrapContentSize(Alignment.BottomStart) + .offset(x = indicatorOffset + 12.dp) + .width(currentTabWidth - 24.dp) + .height(2.dp) +} + +@Composable +fun TabTitle( + title: String, + position: Int, + selected: Boolean, + onClick: (Int) -> Unit, +) { + Text( + text = title, + style = SuwikiTheme.typography.header6, + modifier = Modifier + .wrapContentWidth(Alignment.CenterHorizontally) + .padding(12.dp) + .suwikiClickable( + rippleEnabled = false, + onClick = { onClick(position) }, + ), + color = if (selected) Black else Gray95, + ) +} + +@Preview() +@Composable +fun SuwikiTabBarPreview() { + SuwikiTheme { + var selectedTabPosition by remember { mutableIntStateOf(0) } + + val items = listOf( + "메뉴(0)", "메뉴(0)", + ) + + SuwikiTabBar( + selectedTabPosition = selectedTabPosition, + ) { + items.forEachIndexed { index, s -> + TabTitle(title = s, position = index, selected = index == selectedTabPosition) { selectedTabPosition = index } + } + } + } +} From c823c8f0036fa0e968c683d4de5495f9ebc0da16 Mon Sep 17 00:00:00 2001 From: jinukeu Date: Thu, 28 Dec 2023 19:25:48 +0900 Subject: [PATCH 02/14] =?UTF-8?q?feat/#75:=20OpenMajor=20UI=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=95=84=EC=9B=83=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/designsystem/build.gradle.kts | 1 + .../component/appbar/SuwikiAppBarWithTitle.kt | 45 ++++--- .../component/tabbar/SuwikiTabBar.kt | 2 +- .../feature/openmajor/OpenMajorContract.kt | 10 ++ .../feature/openmajor/OpenMajorScreen.kt | 119 ++++++++++++++++++ .../feature/openmajor/OpenMajorViewModel.kt | 13 ++ .../navigation/OpenMajorNavigation.kt | 35 ++++++ 7 files changed, 204 insertions(+), 21 deletions(-) create mode 100644 feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorContract.kt create mode 100644 feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt create mode 100644 feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt create mode 100644 feature/openmajor/src/main/java/com/suwiki/feature/openmajor/navigation/OpenMajorNavigation.kt diff --git a/core/designsystem/build.gradle.kts b/core/designsystem/build.gradle.kts index 69019aa03..0d4e29846 100644 --- a/core/designsystem/build.gradle.kts +++ b/core/designsystem/build.gradle.kts @@ -12,5 +12,6 @@ dependencies { implementation(projects.core.model) implementation(projects.core.ui) + implementation(libs.kotlinx.immutable) implementation(libs.compose.rating.bar) } diff --git a/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/appbar/SuwikiAppBarWithTitle.kt b/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/appbar/SuwikiAppBarWithTitle.kt index a0ccb0ed6..d9245b626 100644 --- a/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/appbar/SuwikiAppBarWithTitle.kt +++ b/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/appbar/SuwikiAppBarWithTitle.kt @@ -1,8 +1,7 @@ package com.suwiki.core.designsystem.component.appbar import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -11,6 +10,7 @@ import androidx.compose.foundation.shape.CircleShape 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.draw.clip import androidx.compose.ui.res.painterResource @@ -26,40 +26,45 @@ import com.suwiki.core.ui.extension.suwikiClickable fun SuwikiAppBarWithTitle( modifier: Modifier = Modifier, title: String = "", + showBackIcon: Boolean = true, onClickBack: () -> Unit = {}, - onClickRemove: () -> Unit = {}, + onClickClose: () -> Unit = {}, ) { - Row( + Box( modifier = modifier .fillMaxWidth() .wrapContentHeight() .background(White) .padding(top = 15.dp, bottom = 15.dp, start = 18.dp, end = 24.dp), - horizontalArrangement = Arrangement.SpaceBetween, ) { - Icon( - painter = painterResource(id = R.drawable.ic_appbar_arrow_left), - contentDescription = "", - tint = Gray95, - modifier = Modifier - .size(24.dp) - .clip(CircleShape) - .suwikiClickable(onClick = onClickBack) - .padding(vertical = 2.dp, horizontal = 6.5.dp), - ) + if (showBackIcon) { + Icon( + modifier = Modifier + .align(Alignment.CenterStart) + .size(24.dp) + .clip(CircleShape) + .suwikiClickable(onClick = onClickBack) + .padding(vertical = 2.dp, horizontal = 6.5.dp), + painter = painterResource(id = R.drawable.ic_appbar_arrow_left), + contentDescription = "", + tint = Gray95, + ) + } Text( + modifier = Modifier.align(Alignment.Center), text = title, style = SuwikiTheme.typography.header6, ) Icon( - painter = painterResource(id = R.drawable.ic_appbar_close_mark), - contentDescription = "", - tint = Gray95, modifier = Modifier + .align(Alignment.CenterEnd) .size(24.dp) .clip(CircleShape) - .suwikiClickable(onClick = onClickRemove) + .suwikiClickable(onClick = onClickClose) .padding(3.dp), + painter = painterResource(id = R.drawable.ic_appbar_close_mark), + contentDescription = "", + tint = Gray95, ) } } @@ -71,7 +76,7 @@ fun SuwikiAppBarPreview() { SuwikiAppBarWithTitle( title = "타이틀", onClickBack = { /*TODO*/ }, - onClickRemove = { /*TODO*/ }, + onClickClose = { /*TODO*/ }, ) } } diff --git a/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/tabbar/SuwikiTabBar.kt b/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/tabbar/SuwikiTabBar.kt index 981daefce..417a28ede 100644 --- a/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/tabbar/SuwikiTabBar.kt +++ b/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/tabbar/SuwikiTabBar.kt @@ -54,7 +54,7 @@ data class TabPosition( @Composable fun SuwikiTabBar( indicatorColor: Color = Black, - paddingValues: PaddingValues = PaddingValues(start = 24.dp, end = 12.dp), + paddingValues: PaddingValues = PaddingValues(start = 12.dp, end = 12.dp), animationSpec: AnimationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing), selectedTabPosition: Int = 0, tabItem: @Composable () -> Unit, diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorContract.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorContract.kt new file mode 100644 index 000000000..c8ff74966 --- /dev/null +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorContract.kt @@ -0,0 +1,10 @@ +package com.suwiki.feature.openmajor + +data class OpenMajorState( + val selectedOpenMajor: String = "", +) + +sealed interface OpenMajorSideEffect { + data class HandleException(val throwable: Throwable) : OpenMajorSideEffect + data object PopBackStack : OpenMajorSideEffect +} diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt new file mode 100644 index 000000000..0b3965522 --- /dev/null +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt @@ -0,0 +1,119 @@ +package com.suwiki.feature.openmajor + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.suwiki.core.designsystem.component.appbar.SuwikiAppBarWithTitle +import com.suwiki.core.designsystem.component.button.SuwikiContainedLargeButton +import com.suwiki.core.designsystem.component.searchbar.SuwikiSearchBar +import com.suwiki.core.designsystem.component.tabbar.SuwikiTabBar +import com.suwiki.core.designsystem.component.tabbar.TabTitle +import com.suwiki.core.designsystem.shadow.suwikiShadow +import com.suwiki.core.designsystem.theme.SuwikiTheme +import com.suwiki.core.designsystem.theme.White +import org.orbitmvi.orbit.compose.collectAsState +import org.orbitmvi.orbit.compose.collectSideEffect + +private const val OPEN_MAJOR_PAGE_COUNT = 2 + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun OpenMajorRoute( + viewModel: OpenMajorViewModel = hiltViewModel(), + popBackStack: () -> Unit, + handleException: (Throwable) -> Unit, +) { + val uiState = viewModel.collectAsState().value + viewModel.collectSideEffect { sideEffect -> + when (sideEffect) { + is OpenMajorSideEffect.HandleException -> handleException(sideEffect.throwable) + OpenMajorSideEffect.PopBackStack -> popBackStack() + } + } + + val pagerState = rememberPagerState(pageCount = { OPEN_MAJOR_PAGE_COUNT }) + + OpenMajorScreen( + uiState = uiState, + ) +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun OpenMajorScreen( + uiState: OpenMajorState = OpenMajorState(), + pagerState: PagerState = rememberPagerState(pageCount = { OPEN_MAJOR_PAGE_COUNT }), +) { + Box { + Column( + modifier = Modifier.fillMaxSize(), + ) { + SuwikiAppBarWithTitle( + showBackIcon = false, + title = "개설학과", + ) + + SuwikiSearchBar() + + SuwikiTabBar { + TabTitle(title = "전체", position = 0, selected = true, onClick = {}) + TabTitle(title = "즐겨찾기", position = 1, selected = false, onClick = {}) + } + + Spacer(modifier = Modifier.size(12.dp)) + + HorizontalPager( + state = pagerState, + ) { page -> + when (page) { + 0 -> { + } + + 1 -> { + } + } + } + } + + Box( + modifier = Modifier + .padding(24.dp) + .align(Alignment.BottomCenter) + .suwikiShadow( + color = White, + blurRadius = 50.dp, + ) + .height(60.dp), + ) { + SuwikiContainedLargeButton( + modifier = Modifier + .align(Alignment.BottomCenter), + text = "확인", + ) + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Preview(showBackground = true) +@Composable +fun OpenMajorScreenPreview() { + SuwikiTheme { + OpenMajorScreen() + } +} diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt new file mode 100644 index 000000000..05290101b --- /dev/null +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt @@ -0,0 +1,13 @@ +package com.suwiki.feature.openmajor + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.viewmodel.container +import javax.inject.Inject + +@HiltViewModel +class OpenMajorViewModel @Inject constructor() : ContainerHost, ViewModel() { + override val container: Container = container(OpenMajorState()) +} diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/navigation/OpenMajorNavigation.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/navigation/OpenMajorNavigation.kt new file mode 100644 index 000000000..8a8934746 --- /dev/null +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/navigation/OpenMajorNavigation.kt @@ -0,0 +1,35 @@ +package com.suwiki.feature.openmajor.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import com.suwiki.feature.openmajor.OpenMajorRoute + +fun NavController.navigateOpenMajor(selectedOpenMajor: String = "") { + navigate(OpenMajorRoute.route(selectedOpenMajor)) +} + +fun NavGraphBuilder.openMajorNavGraph( + popBackStack: () -> Unit, + handleException: (Throwable) -> Unit, +) { + composable( + route = OpenMajorRoute.route("{selectedOpenMajor}"), + arguments = listOf( + navArgument("selectedOpenMajor") { + type = NavType.StringType + }, + ), + ) { + OpenMajorRoute( + popBackStack = popBackStack, + handleException = handleException, + ) + } +} + +object OpenMajorRoute { + fun route(selectedOpenMajor: String) = "open-major/$selectedOpenMajor" +} From 7d6ce0e9e27ff9ba4f60a6eceab516250db4bae7 Mon Sep 17 00:00:00 2001 From: jinukeu Date: Thu, 28 Dec 2023 22:29:19 +0900 Subject: [PATCH 03/14] =?UTF-8?q?feat/#75:=20OpenMajor=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A4=ED=95=99=EA=B3=BC=20=EB=B6=88=EB=9F=AC=EC=98=A4?= =?UTF-8?q?=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/android/build.gradle.kts | 2 + .../core/android/ThrowUnknownException.kt | 2 + .../suwiki/core/ui/extension/LazyListState.kt | 38 ++++++ .../usecase/GetOpenMajorListUseCase.kt | 3 +- .../suwiki/feature/navigator/MainScreen.kt | 52 ++++---- .../feature/openmajor/OpenMajorContract.kt | 36 +++++- .../feature/openmajor/OpenMajorScreen.kt | 116 ++++++++++++++---- .../feature/openmajor/OpenMajorViewModel.kt | 80 +++++++++++- .../openmajor/component/OpenMajorContainer.kt | 60 +++++---- .../navigation/OpenMajorNavigation.kt | 8 +- 10 files changed, 312 insertions(+), 85 deletions(-) create mode 100644 core/ui/src/main/java/com/suwiki/core/ui/extension/LazyListState.kt rename core/designsystem/src/main/java/com/suwiki/core/designsystem/component/filter/SuwikiFilterContainer.kt => feature/openmajor/src/main/java/com/suwiki/feature/openmajor/component/OpenMajorContainer.kt (67%) diff --git a/core/android/build.gradle.kts b/core/android/build.gradle.kts index 46ce5d669..748e6f18b 100644 --- a/core/android/build.gradle.kts +++ b/core/android/build.gradle.kts @@ -13,4 +13,6 @@ dependencies { implementation(platform(libs.firebase.bom)) implementation(libs.firebase.crashlytics) + + implementation(libs.timber) } diff --git a/core/android/src/main/java/com/suwiki/core/android/ThrowUnknownException.kt b/core/android/src/main/java/com/suwiki/core/android/ThrowUnknownException.kt index 7e64a8095..a3cd25844 100644 --- a/core/android/src/main/java/com/suwiki/core/android/ThrowUnknownException.kt +++ b/core/android/src/main/java/com/suwiki/core/android/ThrowUnknownException.kt @@ -2,9 +2,11 @@ package com.suwiki.core.android import com.google.firebase.crashlytics.ktx.crashlytics import com.google.firebase.ktx.Firebase +import timber.log.Timber fun recordException( e: Throwable, ) { + Timber.e(e) Firebase.crashlytics.recordException(e) } diff --git a/core/ui/src/main/java/com/suwiki/core/ui/extension/LazyListState.kt b/core/ui/src/main/java/com/suwiki/core/ui/extension/LazyListState.kt new file mode 100644 index 000000000..9f56c6e99 --- /dev/null +++ b/core/ui/src/main/java/com/suwiki/core/ui/extension/LazyListState.kt @@ -0,0 +1,38 @@ +package com.suwiki.core.ui.extension + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow + +// https://manavtamboli.medium.com/infinite-list-paged-list-in-jetpack-compose-b10fc7e74768 +@Composable +fun LazyListState.OnBottomReached( + // tells how many items before we reach the bottom of the list + // to call onLoadMore function + buffer: Int = 0, + onLoadMore: () -> Unit, +) { + // Buffer must be positive. + // Or our list will never reach the bottom. + require(buffer >= 0) { "buffer cannot be negative, but was $buffer" } + + val shouldLoadMore = remember { + derivedStateOf { + val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull() + ?: return@derivedStateOf true + + // subtract buffer from the total items + lastVisibleItem.index >= layoutInfo.totalItemsCount - 1 - buffer + } + } + + LaunchedEffect(shouldLoadMore) { + snapshotFlow { shouldLoadMore.value } + .collect { if (it) onLoadMore() } + } +} + +fun LazyListState.isScrolledToEnd() = layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1 diff --git a/domain/openmajor/src/main/java/com/suwiki/domain/openmajor/usecase/GetOpenMajorListUseCase.kt b/domain/openmajor/src/main/java/com/suwiki/domain/openmajor/usecase/GetOpenMajorListUseCase.kt index a27d5a95f..95f590a61 100644 --- a/domain/openmajor/src/main/java/com/suwiki/domain/openmajor/usecase/GetOpenMajorListUseCase.kt +++ b/domain/openmajor/src/main/java/com/suwiki/domain/openmajor/usecase/GetOpenMajorListUseCase.kt @@ -2,6 +2,7 @@ package com.suwiki.domain.openmajor.usecase import com.suwiki.domain.openmajor.repository.OpenMajorRepository import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import javax.inject.Inject class GetOpenMajorListUseCase @Inject constructor( @@ -16,6 +17,6 @@ class GetOpenMajorListUseCase @Inject constructor( * 그 이후 LocalVersion과 LocalOpenMajorList를 최신화 합니다. */ suspend operator fun invoke(): Flow> { - return openMajorRepository.getOpenMajorList() + return openMajorRepository.getOpenMajorList().map { listOf("전체") + it } // TODO v2 api 리팩토링 } } diff --git a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt index 17fad572f..2ac7bcc59 100644 --- a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt +++ b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt @@ -40,6 +40,7 @@ import com.suwiki.feature.login.R import com.suwiki.feature.login.navigation.loginNavGraph import com.suwiki.feature.login.navigation.navigateLogin import com.suwiki.feature.myinfo.navigation.myInfoNavGraph +import com.suwiki.feature.openmajor.OpenMajorRoute import com.suwiki.feature.timetable.navigation.timetableNavGraph import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -56,31 +57,32 @@ internal fun MainScreen( Scaffold( modifier = modifier, content = { innerPadding -> - NavHost( - navController = navigator.navController, - startDestination = navigator.startDestination, - ) { - loginNavGraph( - popBackStack = navigator::popBackStackIfNotHome, - navigateFindId = { /* TODO */ }, - navigateFindPassword = { /* TODO */ }, - navigateSignup = { /* TODO */ }, - handleException = viewModel::handleException, - ) - - timetableNavGraph( - padding = innerPadding, - ) - - lectureEvaluationNavGraph( - padding = innerPadding, - navigateLogin = navigator::navigateLogin, - ) - - myInfoNavGraph( - padding = innerPadding, - ) - } + OpenMajorRoute(popBackStack = { /*TODO*/ }, handleException = {}) +// NavHost( +// navController = navigator.navController, +// startDestination = navigator.startDestination, +// ) { +// loginNavGraph( +// popBackStack = navigator::popBackStackIfNotHome, +// navigateFindId = { /* TODO */ }, +// navigateFindPassword = { /* TODO */ }, +// navigateSignup = { /* TODO */ }, +// handleException = viewModel::handleException, +// ) +// +// timetableNavGraph( +// padding = innerPadding, +// ) +// +// lectureEvaluationNavGraph( +// padding = innerPadding, +// navigateLogin = navigator::navigateLogin, +// ) +// +// myInfoNavGraph( +// padding = innerPadding, +// ) +// } if (uiState.showNetworkErrorDialog) { SuwikiDialog( diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorContract.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorContract.kt index c8ff74966..80e31fbc9 100644 --- a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorContract.kt +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorContract.kt @@ -1,9 +1,43 @@ package com.suwiki.feature.openmajor +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList + data class OpenMajorState( - val selectedOpenMajor: String = "", + val filteredAllOpenMajorList: PersistentList = persistentListOf(), + val filteredBookmarkedOpenMajorList: PersistentList = persistentListOf(), + val showBottomShadow: Boolean = true, + val isLoading: Boolean = false, +) + +data class OpenMajor( + val name: String, + val isBookmarked: Boolean = false, + val isSelected: Boolean = false, ) +fun List.toBookmarkedOpenMajorList( + selectedOpenMajor: String, +) = map { name -> + OpenMajor( + name = name, + isBookmarked = true, + isSelected = selectedOpenMajor == name, + ) +}.toPersistentList() + +fun List.toOpenMajorList( + bookmarkedOpenMajorList: List, + selectedOpenMajor: String, +) = map { name -> + OpenMajor( + name = name, + isBookmarked = name in bookmarkedOpenMajorList, + isSelected = selectedOpenMajor == name, + ) +}.toPersistentList() + sealed interface OpenMajorSideEffect { data class HandleException(val throwable: Throwable) : OpenMajorSideEffect data object PopBackStack : OpenMajorSideEffect diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt index 0b3965522..dc695e94b 100644 --- a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt @@ -1,18 +1,23 @@ package com.suwiki.feature.openmajor import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview @@ -20,14 +25,20 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.suwiki.core.designsystem.component.appbar.SuwikiAppBarWithTitle import com.suwiki.core.designsystem.component.button.SuwikiContainedLargeButton +import com.suwiki.core.designsystem.component.loading.LoadingScreen import com.suwiki.core.designsystem.component.searchbar.SuwikiSearchBar import com.suwiki.core.designsystem.component.tabbar.SuwikiTabBar import com.suwiki.core.designsystem.component.tabbar.TabTitle import com.suwiki.core.designsystem.shadow.suwikiShadow import com.suwiki.core.designsystem.theme.SuwikiTheme import com.suwiki.core.designsystem.theme.White +import com.suwiki.core.ui.extension.OnBottomReached +import com.suwiki.core.ui.extension.isScrolledToEnd +import com.suwiki.feature.openmajor.component.OpenMajorContainer +import kotlinx.collections.immutable.PersistentList import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect +import timber.log.Timber private const val OPEN_MAJOR_PAGE_COUNT = 2 @@ -48,8 +59,38 @@ fun OpenMajorRoute( val pagerState = rememberPagerState(pageCount = { OPEN_MAJOR_PAGE_COUNT }) + val allOpenMajorListState = rememberLazyListState() + val bookmarkedOpenMajorListState = rememberLazyListState() + + val onReachedBottomAllOpenMajor by remember { + derivedStateOf { + allOpenMajorListState.isScrolledToEnd() + } + } + + val onReachedBottomBookmarkedOpenMajor by remember { + derivedStateOf { + bookmarkedOpenMajorListState.isScrolledToEnd() + } + } + + LaunchedEffect(onReachedBottomAllOpenMajor) { + viewModel.changeBottomShadowVisible(!onReachedBottomAllOpenMajor) + } + + LaunchedEffect(onReachedBottomBookmarkedOpenMajor) { + viewModel.changeBottomShadowVisible(!onReachedBottomBookmarkedOpenMajor) + } + + LaunchedEffect(key1 = Unit) { + viewModel.initData() + } + OpenMajorScreen( uiState = uiState, + pagerState = pagerState, + allOpenMajorListState = allOpenMajorListState, + bookmarkedOpenMajorListState = bookmarkedOpenMajorListState, ) } @@ -58,8 +99,12 @@ fun OpenMajorRoute( fun OpenMajorScreen( uiState: OpenMajorState = OpenMajorState(), pagerState: PagerState = rememberPagerState(pageCount = { OPEN_MAJOR_PAGE_COUNT }), + allOpenMajorListState: LazyListState = rememberLazyListState(), + bookmarkedOpenMajorListState: LazyListState = rememberLazyListState(), ) { - Box { + Box( + modifier = Modifier.background(White), + ) { Column( modifier = Modifier.fillMaxSize(), ) { @@ -75,36 +120,63 @@ fun OpenMajorScreen( TabTitle(title = "즐겨찾기", position = 1, selected = false, onClick = {}) } - Spacer(modifier = Modifier.size(12.dp)) - HorizontalPager( + modifier = Modifier.weight(1f), state = pagerState, ) { page -> when (page) { 0 -> { + OpenMajorLazyColumn( + listState = allOpenMajorListState, + openMajorList = uiState.filteredAllOpenMajorList, + ) } 1 -> { + OpenMajorLazyColumn( + listState = bookmarkedOpenMajorListState, + openMajorList = uiState.filteredBookmarkedOpenMajorList, + ) } } } - } - Box( - modifier = Modifier - .padding(24.dp) - .align(Alignment.BottomCenter) - .suwikiShadow( - color = White, - blurRadius = 50.dp, + Box( + modifier = Modifier.padding(start = 24.dp, end = 24.dp, bottom = 24.dp), + ) { + SuwikiContainedLargeButton( + modifier = Modifier + .suwikiShadow( + color = if (uiState.showBottomShadow) White else Color.Transparent, + blurRadius = 80.dp, + spread = 50.dp, + ), + text = "확인", ) - .height(60.dp), - ) { - SuwikiContainedLargeButton( - modifier = Modifier - .align(Alignment.BottomCenter), - text = "확인", - ) + } + } + + if (uiState.isLoading) LoadingScreen() + } +} + +@Composable +private fun OpenMajorLazyColumn( + listState: LazyListState, + openMajorList: PersistentList, +) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + state = listState, + contentPadding = PaddingValues(top = 12.dp), + ) { + items( + count = openMajorList.size, + key = { index -> openMajorList[index].name }, + ) { index -> + with(openMajorList[index]) { + OpenMajorContainer(text = name, isChecked = isSelected, isStared = isBookmarked) + } } } } diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt index 05290101b..12eb1fc93 100644 --- a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt @@ -1,13 +1,91 @@ package com.suwiki.feature.openmajor +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.suwiki.core.model.exception.AuthorizationException +import com.suwiki.domain.openmajor.usecase.GetBookmarkedOpenMajorListUseCase +import com.suwiki.domain.openmajor.usecase.GetOpenMajorListUseCase +import com.suwiki.feature.openmajor.navigation.OpenMajorRoute import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.postSideEffect +import org.orbitmvi.orbit.syntax.simple.reduce import org.orbitmvi.orbit.viewmodel.container +import timber.log.Timber import javax.inject.Inject @HiltViewModel -class OpenMajorViewModel @Inject constructor() : ContainerHost, ViewModel() { +class OpenMajorViewModel @Inject constructor( + private val getOpenMajorListUseCase: GetOpenMajorListUseCase, + private val getBookmarkedOpenMajorListUseCase: GetBookmarkedOpenMajorListUseCase, + savedStateHandle: SavedStateHandle, +) : ContainerHost, ViewModel() { override val container: Container = container(OpenMajorState()) + + private var isLoggedIn: Boolean = true + private val selectedOpenMajor = savedStateHandle[OpenMajorRoute.ARGUMENT_NAME] ?: "전체" + private val allOpenMajorList = mutableListOf() + private val bookmarkedOpenMajorList = mutableListOf() + + fun changeBottomShadowVisible(show: Boolean) = intent { + reduce { state.copy(showBottomShadow = show) } + } + + fun initData() = intent { + viewModelScope.launch { + reduce { state.copy(isLoading = true) } + getOpenMajor() + getBookmarkedOpenMajor() + reduce { state.copy(isLoading = false) } + } + } + + private fun getOpenMajor() = intent { + getOpenMajorListUseCase().onEach { + allOpenMajorList.clear() + allOpenMajorList.addAll(it) + reduceOpenMajorList() + }.catch { + Timber.e(it) + postSideEffect(OpenMajorSideEffect.HandleException(it)) + }.collect() + } + + private fun reduceOpenMajorList() = intent { + reduce { + state.copy( + filteredAllOpenMajorList = allOpenMajorList.toOpenMajorList( + bookmarkedOpenMajorList = bookmarkedOpenMajorList, + selectedOpenMajor = selectedOpenMajor, + ), + ) + } + } + + private suspend fun getBookmarkedOpenMajor() = intent { + getBookmarkedOpenMajorListUseCase() + .onSuccess { + bookmarkedOpenMajorList.addAll(it) + reduceBookmarkedOpenMajorList() + } + .onFailure { + if (it is AuthorizationException) isLoggedIn = false + else postSideEffect(OpenMajorSideEffect.HandleException(it)) + } + } + + private fun reduceBookmarkedOpenMajorList() = intent { + reduce { + state.copy( + filteredBookmarkedOpenMajorList = bookmarkedOpenMajorList.toBookmarkedOpenMajorList(selectedOpenMajor), + ) + } + } } diff --git a/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/filter/SuwikiFilterContainer.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/component/OpenMajorContainer.kt similarity index 67% rename from core/designsystem/src/main/java/com/suwiki/core/designsystem/component/filter/SuwikiFilterContainer.kt rename to feature/openmajor/src/main/java/com/suwiki/feature/openmajor/component/OpenMajorContainer.kt index 51acd60f9..9324c180d 100644 --- a/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/filter/SuwikiFilterContainer.kt +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/component/OpenMajorContainer.kt @@ -1,15 +1,13 @@ -package com.suwiki.core.designsystem.component.filter +package com.suwiki.feature.openmajor.component import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box 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.layout.width -import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -32,7 +30,7 @@ import com.suwiki.core.designsystem.theme.White import com.suwiki.core.ui.extension.suwikiClickable @Composable -fun SuwikiFilterContainer( +fun OpenMajorContainer( modifier: Modifier = Modifier, text: String, isChecked: Boolean, @@ -46,48 +44,46 @@ fun SuwikiFilterContainer( Black to White } - Box( + Row( modifier = modifier .background(backgroundColor) + .padding(vertical = 12.dp, horizontal = 24.dp) .fillMaxWidth() - .wrapContentHeight() - .suwikiClickable(onClick = onClick), + .suwikiClickable( + rippleEnabled = false, + onClick = onClick, + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Start, + Icon( + painter = painterResource(id = R.drawable.ic_filter_stared), + contentDescription = "", modifier = Modifier - .padding(vertical = 12.dp, horizontal = 24.dp), - ) { - Icon( - painter = painterResource(id = R.drawable.ic_filter_stared), - contentDescription = "", - modifier = Modifier - .size(24.dp) - .suwikiClickable( - onClick = onClickStar, - rippleEnabled = false, - ), - tint = if (isStared) Primary else GrayDA, - ) - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = text, - style = SuwikiTheme.typography.body2, - color = textColor, - ) - } + .size(24.dp) + .suwikiClickable( + onClick = onClickStar, + rippleEnabled = false, + ), + tint = if (isStared) Primary else GrayDA, + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = text, + style = SuwikiTheme.typography.body2, + color = textColor, + ) } } @Preview(widthDp = 300, heightDp = 50) @Composable -fun SuwikiFilterContainerPreview() { +fun OpenMajorContainerPreview() { var isChecked by remember { mutableStateOf(false) } var isStared by remember { mutableStateOf(false) } SuwikiTheme { - SuwikiFilterContainer( + OpenMajorContainer( text = "개설학과명", isChecked = isChecked, isStared = isStared, diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/navigation/OpenMajorNavigation.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/navigation/OpenMajorNavigation.kt index 8a8934746..db4e055a2 100644 --- a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/navigation/OpenMajorNavigation.kt +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/navigation/OpenMajorNavigation.kt @@ -16,10 +16,11 @@ fun NavGraphBuilder.openMajorNavGraph( handleException: (Throwable) -> Unit, ) { composable( - route = OpenMajorRoute.route("{selectedOpenMajor}"), + route = OpenMajorRoute.route("{${OpenMajorRoute.ARGUMENT_NAME}}"), arguments = listOf( - navArgument("selectedOpenMajor") { + navArgument(OpenMajorRoute.ARGUMENT_NAME) { type = NavType.StringType + defaultValue = "전체" }, ), ) { @@ -31,5 +32,6 @@ fun NavGraphBuilder.openMajorNavGraph( } object OpenMajorRoute { - fun route(selectedOpenMajor: String) = "open-major/$selectedOpenMajor" + const val ARGUMENT_NAME = "selectedOpenMajor" + fun route(selectedOpenMajor: String = "전체") = "open-major/$selectedOpenMajor" } From 225bf19d471d64449ccdf1964967d263cb4a43b5 Mon Sep 17 00:00:00 2001 From: jinukeu Date: Fri, 29 Dec 2023 10:09:17 +0900 Subject: [PATCH 04/14] =?UTF-8?q?feat/#75:=20OpenMajor=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LectureEvaluationContract.kt | 1 + .../viewerreporter/LectureEvaluationScreen.kt | 17 +++++ .../LectureEvaluationViewModel.kt | 8 ++- .../navigation/LectureEvaluationNavigation.kt | 8 ++- .../suwiki/feature/navigator/MainNavigator.kt | 5 ++ .../suwiki/feature/navigator/MainScreen.kt | 67 ++++++++++++------- .../feature/openmajor/OpenMajorContract.kt | 30 +-------- .../feature/openmajor/OpenMajorScreen.kt | 26 +++++-- .../feature/openmajor/OpenMajorViewModel.kt | 20 ++++-- .../feature/openmajor/model/OpenMajor.kt | 30 +++++++++ .../navigation/OpenMajorNavigation.kt | 2 + .../openmajor/src/main/res/values/strings.xml | 4 ++ 12 files changed, 151 insertions(+), 67 deletions(-) create mode 100644 feature/openmajor/src/main/java/com/suwiki/feature/openmajor/model/OpenMajor.kt create mode 100644 feature/openmajor/src/main/res/values/strings.xml diff --git a/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationContract.kt b/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationContract.kt index a41ee50df..2b50725f0 100644 --- a/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationContract.kt +++ b/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationContract.kt @@ -2,6 +2,7 @@ package com.suwiki.feature.lectureevaluation.viewerreporter data class LectureEvaluationState( val showOnboardingBottomSheet: Boolean = false, + val selectedOpenMajor: String = "", ) sealed interface LectureEvaluationSideEffect { diff --git a/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationScreen.kt b/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationScreen.kt index 203f10ead..4df6859e0 100644 --- a/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationScreen.kt +++ b/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier @@ -14,6 +15,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.suwiki.core.designsystem.theme.SuwikiTheme +import com.suwiki.core.ui.extension.suwikiClickable import com.suwiki.feature.lectureevaluation.viewerreporter.component.ONBOARDING_PAGE_COUNT import com.suwiki.feature.lectureevaluation.viewerreporter.component.OnboardingBottomSheet import org.orbitmvi.orbit.compose.collectAsState @@ -24,7 +26,9 @@ import org.orbitmvi.orbit.compose.collectSideEffect fun LectureEvaluationRoute( padding: PaddingValues, viewModel: LectureEvaluationViewModel = hiltViewModel(), + selectedOpenMajor: String, navigateLogin: () -> Unit, + navigateOpenMajor: (String) -> Unit, ) { val uiState = viewModel.collectAsState().value viewModel.collectSideEffect { sideEffect -> @@ -38,6 +42,10 @@ fun LectureEvaluationRoute( viewModel.checkLoggedInShowBottomSheetIfNeed() } + LaunchedEffect(key1 = selectedOpenMajor) { + viewModel.updateSelectedOpenMajor(selectedOpenMajor) + } + val pagerState = rememberPagerState(pageCount = { ONBOARDING_PAGE_COUNT }) LectureEvaluationScreen( @@ -49,6 +57,7 @@ fun LectureEvaluationRoute( viewModel.hideOnboardingBottomSheet() viewModel.navigateLogin() }, + onClickTempText = navigateOpenMajor, ) } @@ -61,8 +70,16 @@ fun LectureEvaluationScreen( hideOnboardingBottomSheet: () -> Unit = {}, onClickLoginButton: () -> Unit = {}, onClickSignupButton: () -> Unit = {}, + onClickTempText: (String) -> Unit = {}, // TODO 개설학과 선택 페이지로 임시로 넘어가기 위한 람다입니다. 마음대로 삭제 가능. ) { Box(modifier = Modifier.fillMaxSize().padding(padding)) { + Text( + modifier = Modifier.suwikiClickable( + onClick = { onClickTempText(uiState.selectedOpenMajor) }, + ), + text = uiState.selectedOpenMajor, + ) + OnboardingBottomSheet( uiState = uiState, hideOnboardingBottomSheet = hideOnboardingBottomSheet, diff --git a/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationViewModel.kt b/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationViewModel.kt index 850f290f8..b6c74c9f6 100644 --- a/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationViewModel.kt +++ b/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationViewModel.kt @@ -25,9 +25,7 @@ class LectureEvaluationViewModel @Inject constructor( private var isLoggedIn: Boolean = false private var isFirstVisit: Boolean = true - private suspend fun checkLoggedIn() { - isLoggedIn = getUserInfoUseCase().catch { }.lastOrNull()?.isLoggedIn == true - } + fun updateSelectedOpenMajor(openMajor: String) = intent { reduce { state.copy(selectedOpenMajor = openMajor) } } fun checkLoggedInShowBottomSheetIfNeed() = viewModelScope.launch { checkLoggedIn() @@ -37,6 +35,10 @@ class LectureEvaluationViewModel @Inject constructor( } } + private suspend fun checkLoggedIn() { + isLoggedIn = getUserInfoUseCase().catch { }.lastOrNull()?.isLoggedIn == true + } + fun navigateLogin() = intent { postSideEffect(LectureEvaluationSideEffect.NavigateLogin) } private fun showOnboardingBottomSheet() = intent { reduce { state.copy(showOnboardingBottomSheet = true) } } diff --git a/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/navigation/LectureEvaluationNavigation.kt b/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/navigation/LectureEvaluationNavigation.kt index 591c7e837..2edd0b164 100644 --- a/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/navigation/LectureEvaluationNavigation.kt +++ b/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/navigation/LectureEvaluationNavigation.kt @@ -6,6 +6,7 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.suwiki.feature.lectureevaluation.viewerreporter.LectureEvaluationRoute +import timber.log.Timber fun NavController.navigateLectureEvaluation(navOptions: NavOptions) { navigate(LectureEvaluationRoute.route, navOptions) @@ -13,12 +14,17 @@ fun NavController.navigateLectureEvaluation(navOptions: NavOptions) { fun NavGraphBuilder.lectureEvaluationNavGraph( padding: PaddingValues, + argumentName: String, navigateLogin: () -> Unit, + navigateOpenMajor: (String) -> Unit, ) { - composable(route = LectureEvaluationRoute.route) { + composable(route = LectureEvaluationRoute.route) { navBackStackEntry -> + val selectedOpenMajor = navBackStackEntry.savedStateHandle.get(argumentName) ?: "전체" LectureEvaluationRoute( padding = padding, + selectedOpenMajor = selectedOpenMajor, navigateLogin = navigateLogin, + navigateOpenMajor = navigateOpenMajor, ) } } diff --git a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainNavigator.kt b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainNavigator.kt index 2873a2f22..2b0aa553d 100644 --- a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainNavigator.kt +++ b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainNavigator.kt @@ -11,6 +11,7 @@ import androidx.navigation.navOptions import com.suwiki.feature.lectureevaluation.viewerreporter.navigation.navigateLectureEvaluation import com.suwiki.feature.login.navigation.navigateLogin import com.suwiki.feature.myinfo.navigation.navigateMyInfo +import com.suwiki.feature.openmajor.navigation.navigateOpenMajor import com.suwiki.feature.timetable.navigation.TimetableRoute import com.suwiki.feature.timetable.navigation.navigateTimetable @@ -48,6 +49,10 @@ internal class MainNavigator( navController.navigateLogin() } + fun navigateOpenMajor(selectedOpenMajor: String) { + navController.navigateOpenMajor(selectedOpenMajor) + } + fun popBackStackIfNotHome() { if (!isSameCurrentDestination(TimetableRoute.route)) { navController.popBackStack() diff --git a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt index 2ac7bcc59..94a2cc59f 100644 --- a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt +++ b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt @@ -41,6 +41,8 @@ import com.suwiki.feature.login.navigation.loginNavGraph import com.suwiki.feature.login.navigation.navigateLogin import com.suwiki.feature.myinfo.navigation.myInfoNavGraph import com.suwiki.feature.openmajor.OpenMajorRoute +import com.suwiki.feature.openmajor.navigation.OpenMajorRoute +import com.suwiki.feature.openmajor.navigation.openMajorNavGraph import com.suwiki.feature.timetable.navigation.timetableNavGraph import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -57,32 +59,45 @@ internal fun MainScreen( Scaffold( modifier = modifier, content = { innerPadding -> - OpenMajorRoute(popBackStack = { /*TODO*/ }, handleException = {}) -// NavHost( -// navController = navigator.navController, -// startDestination = navigator.startDestination, -// ) { -// loginNavGraph( -// popBackStack = navigator::popBackStackIfNotHome, -// navigateFindId = { /* TODO */ }, -// navigateFindPassword = { /* TODO */ }, -// navigateSignup = { /* TODO */ }, -// handleException = viewModel::handleException, -// ) -// -// timetableNavGraph( -// padding = innerPadding, -// ) -// -// lectureEvaluationNavGraph( -// padding = innerPadding, -// navigateLogin = navigator::navigateLogin, -// ) -// -// myInfoNavGraph( -// padding = innerPadding, -// ) -// } + NavHost( + navController = navigator.navController, + startDestination = navigator.startDestination, + ) { + loginNavGraph( + popBackStack = navigator::popBackStackIfNotHome, + navigateFindId = { /* TODO */ }, + navigateFindPassword = { /* TODO */ }, + navigateSignup = { /* TODO */ }, + handleException = viewModel::handleException, + ) + + openMajorNavGraph( + popBackStack = navigator::popBackStackIfNotHome, + popBackStackWithArgument = { openMajor -> + navigator.navController.previousBackStackEntry?.savedStateHandle?.set( + OpenMajorRoute.ARGUMENT_NAME, + openMajor, + ) + navigator.popBackStackIfNotHome() + }, + handleException = viewModel::handleException, + ) + + timetableNavGraph( + padding = innerPadding, + ) + + lectureEvaluationNavGraph( + padding = innerPadding, + argumentName = OpenMajorRoute.ARGUMENT_NAME, + navigateLogin = navigator::navigateLogin, + navigateOpenMajor = navigator::navigateOpenMajor, + ) + + myInfoNavGraph( + padding = innerPadding, + ) + } if (uiState.showNetworkErrorDialog) { SuwikiDialog( diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorContract.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorContract.kt index 80e31fbc9..0476087c0 100644 --- a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorContract.kt +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorContract.kt @@ -1,8 +1,8 @@ package com.suwiki.feature.openmajor +import com.suwiki.feature.openmajor.model.OpenMajor import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList data class OpenMajorState( val filteredAllOpenMajorList: PersistentList = persistentListOf(), @@ -11,34 +11,8 @@ data class OpenMajorState( val isLoading: Boolean = false, ) -data class OpenMajor( - val name: String, - val isBookmarked: Boolean = false, - val isSelected: Boolean = false, -) - -fun List.toBookmarkedOpenMajorList( - selectedOpenMajor: String, -) = map { name -> - OpenMajor( - name = name, - isBookmarked = true, - isSelected = selectedOpenMajor == name, - ) -}.toPersistentList() - -fun List.toOpenMajorList( - bookmarkedOpenMajorList: List, - selectedOpenMajor: String, -) = map { name -> - OpenMajor( - name = name, - isBookmarked = name in bookmarkedOpenMajorList, - isSelected = selectedOpenMajor == name, - ) -}.toPersistentList() - sealed interface OpenMajorSideEffect { data class HandleException(val throwable: Throwable) : OpenMajorSideEffect data object PopBackStack : OpenMajorSideEffect + data class PopBackStackWithArgument(val argument: String) : OpenMajorSideEffect } diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt index dc695e94b..dd43237ee 100644 --- a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -32,13 +33,12 @@ import com.suwiki.core.designsystem.component.tabbar.TabTitle import com.suwiki.core.designsystem.shadow.suwikiShadow import com.suwiki.core.designsystem.theme.SuwikiTheme import com.suwiki.core.designsystem.theme.White -import com.suwiki.core.ui.extension.OnBottomReached import com.suwiki.core.ui.extension.isScrolledToEnd import com.suwiki.feature.openmajor.component.OpenMajorContainer +import com.suwiki.feature.openmajor.model.OpenMajor import kotlinx.collections.immutable.PersistentList import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect -import timber.log.Timber private const val OPEN_MAJOR_PAGE_COUNT = 2 @@ -47,6 +47,7 @@ private const val OPEN_MAJOR_PAGE_COUNT = 2 fun OpenMajorRoute( viewModel: OpenMajorViewModel = hiltViewModel(), popBackStack: () -> Unit, + popBackStackWithArgument: (String) -> Unit, handleException: (Throwable) -> Unit, ) { val uiState = viewModel.collectAsState().value @@ -54,6 +55,7 @@ fun OpenMajorRoute( when (sideEffect) { is OpenMajorSideEffect.HandleException -> handleException(sideEffect.throwable) OpenMajorSideEffect.PopBackStack -> popBackStack() + is OpenMajorSideEffect.PopBackStackWithArgument -> popBackStackWithArgument(sideEffect.argument) } } @@ -91,6 +93,9 @@ fun OpenMajorRoute( pagerState = pagerState, allOpenMajorListState = allOpenMajorListState, bookmarkedOpenMajorListState = bookmarkedOpenMajorListState, + onClickClose = viewModel::popBackStack, + onClickConfirmButton = viewModel::popBackStackWithArgument, + onClickOpenMajorContainer = viewModel::updateSelectedOpenMajor, ) } @@ -101,6 +106,9 @@ fun OpenMajorScreen( pagerState: PagerState = rememberPagerState(pageCount = { OPEN_MAJOR_PAGE_COUNT }), allOpenMajorListState: LazyListState = rememberLazyListState(), bookmarkedOpenMajorListState: LazyListState = rememberLazyListState(), + onClickClose: () -> Unit = {}, + onClickConfirmButton: () -> Unit = {}, + onClickOpenMajorContainer: (String) -> Unit = {}, ) { Box( modifier = Modifier.background(White), @@ -111,6 +119,7 @@ fun OpenMajorScreen( SuwikiAppBarWithTitle( showBackIcon = false, title = "개설학과", + onClickClose = onClickClose, ) SuwikiSearchBar() @@ -129,6 +138,7 @@ fun OpenMajorScreen( OpenMajorLazyColumn( listState = allOpenMajorListState, openMajorList = uiState.filteredAllOpenMajorList, + onClickOpenMajorContainer = onClickOpenMajorContainer, ) } @@ -136,6 +146,7 @@ fun OpenMajorScreen( OpenMajorLazyColumn( listState = bookmarkedOpenMajorListState, openMajorList = uiState.filteredBookmarkedOpenMajorList, + onClickOpenMajorContainer = onClickOpenMajorContainer, ) } } @@ -151,7 +162,8 @@ fun OpenMajorScreen( blurRadius = 80.dp, spread = 50.dp, ), - text = "확인", + text = stringResource(R.string.word_confirm), + onClick = onClickConfirmButton, ) } } @@ -164,6 +176,7 @@ fun OpenMajorScreen( private fun OpenMajorLazyColumn( listState: LazyListState, openMajorList: PersistentList, + onClickOpenMajorContainer: (String) -> Unit = {}, ) { LazyColumn( modifier = Modifier.fillMaxSize(), @@ -175,7 +188,12 @@ private fun OpenMajorLazyColumn( key = { index -> openMajorList[index].name }, ) { index -> with(openMajorList[index]) { - OpenMajorContainer(text = name, isChecked = isSelected, isStared = isBookmarked) + OpenMajorContainer( + text = name, + isChecked = isSelected, + isStared = isBookmarked, + onClick = { onClickOpenMajorContainer(name) }, + ) } } } diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt index 12eb1fc93..9c5930d30 100644 --- a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt @@ -6,10 +6,12 @@ import androidx.lifecycle.viewModelScope import com.suwiki.core.model.exception.AuthorizationException import com.suwiki.domain.openmajor.usecase.GetBookmarkedOpenMajorListUseCase import com.suwiki.domain.openmajor.usecase.GetOpenMajorListUseCase +import com.suwiki.feature.openmajor.model.toBookmarkedOpenMajorList +import com.suwiki.feature.openmajor.model.toOpenMajorList import com.suwiki.feature.openmajor.navigation.OpenMajorRoute import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.orbitmvi.orbit.Container @@ -18,7 +20,6 @@ import org.orbitmvi.orbit.syntax.simple.intent import org.orbitmvi.orbit.syntax.simple.postSideEffect import org.orbitmvi.orbit.syntax.simple.reduce import org.orbitmvi.orbit.viewmodel.container -import timber.log.Timber import javax.inject.Inject @HiltViewModel @@ -30,10 +31,20 @@ class OpenMajorViewModel @Inject constructor( override val container: Container = container(OpenMajorState()) private var isLoggedIn: Boolean = true - private val selectedOpenMajor = savedStateHandle[OpenMajorRoute.ARGUMENT_NAME] ?: "전체" + private var selectedOpenMajor = savedStateHandle[OpenMajorRoute.ARGUMENT_NAME] ?: "전체" private val allOpenMajorList = mutableListOf() private val bookmarkedOpenMajorList = mutableListOf() + fun updateSelectedOpenMajor(openMajor: String) = intent { + selectedOpenMajor = openMajor + reduceOpenMajorList() + reduceBookmarkedOpenMajorList() + } + + fun popBackStack() = intent { postSideEffect(OpenMajorSideEffect.PopBackStack) } + + fun popBackStackWithArgument() = intent { postSideEffect(OpenMajorSideEffect.PopBackStackWithArgument(selectedOpenMajor)) } + fun changeBottomShadowVisible(show: Boolean) = intent { reduce { state.copy(showBottomShadow = show) } } @@ -53,9 +64,8 @@ class OpenMajorViewModel @Inject constructor( allOpenMajorList.addAll(it) reduceOpenMajorList() }.catch { - Timber.e(it) postSideEffect(OpenMajorSideEffect.HandleException(it)) - }.collect() + }.launchIn(viewModelScope) } private fun reduceOpenMajorList() = intent { diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/model/OpenMajor.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/model/OpenMajor.kt new file mode 100644 index 000000000..2a16177ba --- /dev/null +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/model/OpenMajor.kt @@ -0,0 +1,30 @@ +package com.suwiki.feature.openmajor.model + +import kotlinx.collections.immutable.toPersistentList + +data class OpenMajor( + val name: String, + val isBookmarked: Boolean = false, + val isSelected: Boolean = false, +) + +fun List.toBookmarkedOpenMajorList( + selectedOpenMajor: String, +) = map { name -> + OpenMajor( + name = name, + isBookmarked = true, + isSelected = selectedOpenMajor == name, + ) +}.toPersistentList() + +fun List.toOpenMajorList( + bookmarkedOpenMajorList: List, + selectedOpenMajor: String, +) = map { name -> + OpenMajor( + name = name, + isBookmarked = name in bookmarkedOpenMajorList, + isSelected = selectedOpenMajor == name, + ) +}.toPersistentList() diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/navigation/OpenMajorNavigation.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/navigation/OpenMajorNavigation.kt index db4e055a2..404d4f967 100644 --- a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/navigation/OpenMajorNavigation.kt +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/navigation/OpenMajorNavigation.kt @@ -13,6 +13,7 @@ fun NavController.navigateOpenMajor(selectedOpenMajor: String = "") { fun NavGraphBuilder.openMajorNavGraph( popBackStack: () -> Unit, + popBackStackWithArgument: (String) -> Unit, handleException: (Throwable) -> Unit, ) { composable( @@ -26,6 +27,7 @@ fun NavGraphBuilder.openMajorNavGraph( ) { OpenMajorRoute( popBackStack = popBackStack, + popBackStackWithArgument = popBackStackWithArgument, handleException = handleException, ) } diff --git a/feature/openmajor/src/main/res/values/strings.xml b/feature/openmajor/src/main/res/values/strings.xml new file mode 100644 index 000000000..109104213 --- /dev/null +++ b/feature/openmajor/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + 확인 + From 9595fb5deb0d8682f0b0de9444bec9cfcce2fcb1 Mon Sep 17 00:00:00 2001 From: jinukeu Date: Fri, 29 Dec 2023 16:43:15 +0900 Subject: [PATCH 05/14] =?UTF-8?q?feat/#75:=20OpenMajor=20=EB=B6=81?= =?UTF-8?q?=EB=A7=88=ED=81=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/suwiki/core/ui/extension/Flow.kt | 26 ++++++++ .../repository/OpenMajorRepositoryImpl.kt | 1 + .../suwiki/feature/navigator/MainScreen.kt | 3 + .../suwiki/feature/navigator/MainViewModel.kt | 4 +- .../feature/openmajor/OpenMajorContract.kt | 3 + .../feature/openmajor/OpenMajorScreen.kt | 55 +++++++++++++--- .../feature/openmajor/OpenMajorViewModel.kt | 64 +++++++++++++++++-- .../feature/openmajor/model/OpenMajor.kt | 2 + .../feature/openmajor/model/OpenMajorTap.kt | 18 ++++++ .../navigation/OpenMajorNavigation.kt | 2 + .../openmajor/src/main/res/values/strings.xml | 3 + .../suwiki/remote/openmajor/api/MajorApi.kt | 4 +- .../RemoteOpenMajorDataSourceImpl.kt | 3 +- .../openmajor/request/BookmarkMajorRequest.kt | 3 + 14 files changed, 170 insertions(+), 21 deletions(-) create mode 100644 core/ui/src/main/java/com/suwiki/core/ui/extension/Flow.kt create mode 100644 feature/openmajor/src/main/java/com/suwiki/feature/openmajor/model/OpenMajorTap.kt diff --git a/core/ui/src/main/java/com/suwiki/core/ui/extension/Flow.kt b/core/ui/src/main/java/com/suwiki/core/ui/extension/Flow.kt new file mode 100644 index 000000000..4550d5f9b --- /dev/null +++ b/core/ui/src/main/java/com/suwiki/core/ui/extension/Flow.kt @@ -0,0 +1,26 @@ +package com.suwiki.core.ui.extension + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch + +@Composable +inline fun Flow.collectWithLifecycle( + lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, + minActiveState: Lifecycle.State = Lifecycle.State.STARTED, + noinline action: suspend (T) -> Unit, +) { + LaunchedEffect(key1 = Unit) { + lifecycleOwner.lifecycleScope.launch { + flowWithLifecycle(lifecycleOwner.lifecycle, minActiveState).collect { + action(it) + } + } + } +} diff --git a/data/openmajor/src/main/java/com/suwiki/data/openmajor/repository/OpenMajorRepositoryImpl.kt b/data/openmajor/src/main/java/com/suwiki/data/openmajor/repository/OpenMajorRepositoryImpl.kt index 5c50c706f..72b5994ea 100644 --- a/data/openmajor/src/main/java/com/suwiki/data/openmajor/repository/OpenMajorRepositoryImpl.kt +++ b/data/openmajor/src/main/java/com/suwiki/data/openmajor/repository/OpenMajorRepositoryImpl.kt @@ -4,6 +4,7 @@ import com.suwiki.core.model.openmajor.OpenMajor import com.suwiki.data.openmajor.datasource.LocalOpenMajorDataSource import com.suwiki.data.openmajor.datasource.RemoteOpenMajorDataSource import com.suwiki.domain.openmajor.repository.OpenMajorRepository +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flow diff --git a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt index 94a2cc59f..a591e51f9 100644 --- a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt +++ b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt @@ -18,6 +18,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -81,6 +83,7 @@ internal fun MainScreen( navigator.popBackStackIfNotHome() }, handleException = viewModel::handleException, + onShowToast = viewModel::onShowToast, ) timetableNavGraph( diff --git a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainViewModel.kt b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainViewModel.kt index 49520fbb0..e7cd6b1b5 100644 --- a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainViewModel.kt +++ b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainViewModel.kt @@ -25,13 +25,13 @@ class MainViewModel @Inject constructor() : ContainerHost = persistentListOf(), val filteredBookmarkedOpenMajorList: PersistentList = persistentListOf(), val showBottomShadow: Boolean = true, + val currentPage: Int = 0, val isLoading: Boolean = false, ) @@ -15,4 +16,6 @@ sealed interface OpenMajorSideEffect { data class HandleException(val throwable: Throwable) : OpenMajorSideEffect data object PopBackStack : OpenMajorSideEffect data class PopBackStackWithArgument(val argument: String) : OpenMajorSideEffect + + data object ShowNeedLoginToast : OpenMajorSideEffect } diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt index dd43237ee..a3a87e204 100644 --- a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt @@ -18,8 +18,11 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -33,14 +36,18 @@ import com.suwiki.core.designsystem.component.tabbar.TabTitle import com.suwiki.core.designsystem.shadow.suwikiShadow import com.suwiki.core.designsystem.theme.SuwikiTheme import com.suwiki.core.designsystem.theme.White +import com.suwiki.core.ui.extension.collectWithLifecycle import com.suwiki.core.ui.extension.isScrolledToEnd import com.suwiki.feature.openmajor.component.OpenMajorContainer import com.suwiki.feature.openmajor.model.OpenMajor +import com.suwiki.feature.openmajor.model.OpenMajorTap import kotlinx.collections.immutable.PersistentList +import kotlinx.coroutines.launch import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect +import timber.log.Timber -private const val OPEN_MAJOR_PAGE_COUNT = 2 +private val OPEN_MAJOR_PAGE_COUNT = OpenMajorTap.entries.size @OptIn(ExperimentalFoundationApi::class) @Composable @@ -49,18 +56,32 @@ fun OpenMajorRoute( popBackStack: () -> Unit, popBackStackWithArgument: (String) -> Unit, handleException: (Throwable) -> Unit, + onShowToast: (String) -> Unit, ) { val uiState = viewModel.collectAsState().value + + val context = LocalContext.current viewModel.collectSideEffect { sideEffect -> when (sideEffect) { is OpenMajorSideEffect.HandleException -> handleException(sideEffect.throwable) OpenMajorSideEffect.PopBackStack -> popBackStack() is OpenMajorSideEffect.PopBackStackWithArgument -> popBackStackWithArgument(sideEffect.argument) + is OpenMajorSideEffect.ShowNeedLoginToast -> { + onShowToast(context.getString(R.string.open_major_need_login_toast)) + } } } val pagerState = rememberPagerState(pageCount = { OPEN_MAJOR_PAGE_COUNT }) + LaunchedEffect(key1 = uiState.currentPage) { + pagerState.animateScrollToPage(uiState.currentPage) + } + + snapshotFlow { pagerState.currentPage }.collectWithLifecycle { + viewModel.syncPagerState(it) + } + val allOpenMajorListState = rememberLazyListState() val bookmarkedOpenMajorListState = rememberLazyListState() @@ -96,6 +117,8 @@ fun OpenMajorRoute( onClickClose = viewModel::popBackStack, onClickConfirmButton = viewModel::popBackStackWithArgument, onClickOpenMajorContainer = viewModel::updateSelectedOpenMajor, + onClickOpenMajorBookmark = viewModel::registerOrUnRegisterBookmark, + onClickTab = viewModel::syncPagerState, ) } @@ -109,6 +132,8 @@ fun OpenMajorScreen( onClickClose: () -> Unit = {}, onClickConfirmButton: () -> Unit = {}, onClickOpenMajorContainer: (String) -> Unit = {}, + onClickOpenMajorBookmark: (String) -> Unit = {}, + onClickTab: (Int) -> Unit = {}, ) { Box( modifier = Modifier.background(White), @@ -124,29 +149,41 @@ fun OpenMajorScreen( SuwikiSearchBar() - SuwikiTabBar { - TabTitle(title = "전체", position = 0, selected = true, onClick = {}) - TabTitle(title = "즐겨찾기", position = 1, selected = false, onClick = {}) + SuwikiTabBar( + selectedTabPosition = pagerState.currentPage, + ) { + OpenMajorTap.entries.forEach { tab -> + with(tab) { + TabTitle( + title = stringResource(id = titleId), + position = position, + selected = pagerState.currentPage == position, + onClick = { onClickTab(position) }, + ) + } + } } HorizontalPager( modifier = Modifier.weight(1f), state = pagerState, ) { page -> - when (page) { - 0 -> { + when (OpenMajorTap.entries[page]) { + OpenMajorTap.ALL -> { OpenMajorLazyColumn( listState = allOpenMajorListState, openMajorList = uiState.filteredAllOpenMajorList, onClickOpenMajorContainer = onClickOpenMajorContainer, + onClickOpenMajorBookmark = onClickOpenMajorBookmark, ) } - 1 -> { + OpenMajorTap.BOOKMARK -> { OpenMajorLazyColumn( listState = bookmarkedOpenMajorListState, openMajorList = uiState.filteredBookmarkedOpenMajorList, onClickOpenMajorContainer = onClickOpenMajorContainer, + onClickOpenMajorBookmark = onClickOpenMajorBookmark, ) } } @@ -177,6 +214,7 @@ private fun OpenMajorLazyColumn( listState: LazyListState, openMajorList: PersistentList, onClickOpenMajorContainer: (String) -> Unit = {}, + onClickOpenMajorBookmark: (String) -> Unit = {}, ) { LazyColumn( modifier = Modifier.fillMaxSize(), @@ -185,7 +223,7 @@ private fun OpenMajorLazyColumn( ) { items( count = openMajorList.size, - key = { index -> openMajorList[index].name }, + key = { index -> openMajorList[index].id }, ) { index -> with(openMajorList[index]) { OpenMajorContainer( @@ -193,6 +231,7 @@ private fun OpenMajorLazyColumn( isChecked = isSelected, isStared = isBookmarked, onClick = { onClickOpenMajorContainer(name) }, + onClickStar = { onClickOpenMajorBookmark(name) } ) } } diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt index 9c5930d30..b07b184bf 100644 --- a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt @@ -6,13 +6,18 @@ import androidx.lifecycle.viewModelScope import com.suwiki.core.model.exception.AuthorizationException import com.suwiki.domain.openmajor.usecase.GetBookmarkedOpenMajorListUseCase import com.suwiki.domain.openmajor.usecase.GetOpenMajorListUseCase +import com.suwiki.domain.openmajor.usecase.RegisterBookmarkUseCase +import com.suwiki.domain.openmajor.usecase.UnRegisterBookmarkUseCase +import com.suwiki.feature.openmajor.model.OpenMajorTap import com.suwiki.feature.openmajor.model.toBookmarkedOpenMajorList import com.suwiki.feature.openmajor.model.toOpenMajorList import com.suwiki.feature.openmajor.navigation.OpenMajorRoute import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost @@ -26,6 +31,8 @@ import javax.inject.Inject class OpenMajorViewModel @Inject constructor( private val getOpenMajorListUseCase: GetOpenMajorListUseCase, private val getBookmarkedOpenMajorListUseCase: GetBookmarkedOpenMajorListUseCase, + private val registerBookmarkUseCase: RegisterBookmarkUseCase, + private val unRegisterBookmarkUseCase: UnRegisterBookmarkUseCase, savedStateHandle: SavedStateHandle, ) : ContainerHost, ViewModel() { override val container: Container = container(OpenMajorState()) @@ -35,6 +42,51 @@ class OpenMajorViewModel @Inject constructor( private val allOpenMajorList = mutableListOf() private val bookmarkedOpenMajorList = mutableListOf() + fun registerOrUnRegisterBookmark(openMajor: String) = intent { + if (isLoggedIn.not()) { + postSideEffect(OpenMajorSideEffect.ShowNeedLoginToast) + return@intent + } + + if (openMajor in bookmarkedOpenMajorList) { + unRegisterBookmark(openMajor) + } else { + registerBookmark(openMajor) + } + } + + private fun registerBookmark(openMajor: String) = intent { + registerBookmarkUseCase(openMajor) + .onSuccess { + bookmarkedOpenMajorList.add(openMajor) + reduceBookmarkedOpenMajorList() + reduceOpenMajorList() + } + .onFailure { + postSideEffect(OpenMajorSideEffect.HandleException(it)) + } + } + + private fun unRegisterBookmark(openMajor: String) = intent { + unRegisterBookmarkUseCase(openMajor) + .onSuccess { + bookmarkedOpenMajorList.remove(openMajor) + reduceBookmarkedOpenMajorList() + reduceOpenMajorList() + } + .onFailure { + postSideEffect(OpenMajorSideEffect.HandleException(it)) + } + } + + fun syncPagerState(currentPage: Int) = intent { + if (isLoggedIn.not() && currentPage == OpenMajorTap.BOOKMARK.position) { + postSideEffect(OpenMajorSideEffect.ShowNeedLoginToast) + return@intent + } + reduce { state.copy(currentPage = currentPage) } + } + fun updateSelectedOpenMajor(openMajor: String) = intent { selectedOpenMajor = openMajor reduceOpenMajorList() @@ -50,12 +102,9 @@ class OpenMajorViewModel @Inject constructor( } fun initData() = intent { - viewModelScope.launch { - reduce { state.copy(isLoading = true) } - getOpenMajor() - getBookmarkedOpenMajor() - reduce { state.copy(isLoading = false) } - } + reduce { state.copy(isLoading = true) } + joinAll(getOpenMajor(), getBookmarkedOpenMajor()) + reduce { state.copy(isLoading = false) } } private fun getOpenMajor() = intent { @@ -79,11 +128,12 @@ class OpenMajorViewModel @Inject constructor( } } - private suspend fun getBookmarkedOpenMajor() = intent { + private fun getBookmarkedOpenMajor() = intent { getBookmarkedOpenMajorListUseCase() .onSuccess { bookmarkedOpenMajorList.addAll(it) reduceBookmarkedOpenMajorList() + reduceOpenMajorList() } .onFailure { if (it is AuthorizationException) isLoggedIn = false diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/model/OpenMajor.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/model/OpenMajor.kt index 2a16177ba..0de396027 100644 --- a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/model/OpenMajor.kt +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/model/OpenMajor.kt @@ -1,8 +1,10 @@ package com.suwiki.feature.openmajor.model import kotlinx.collections.immutable.toPersistentList +import java.util.UUID data class OpenMajor( + val id: UUID = UUID.randomUUID(), val name: String, val isBookmarked: Boolean = false, val isSelected: Boolean = false, diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/model/OpenMajorTap.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/model/OpenMajorTap.kt new file mode 100644 index 000000000..f3c03b2de --- /dev/null +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/model/OpenMajorTap.kt @@ -0,0 +1,18 @@ +package com.suwiki.feature.openmajor.model + +import androidx.annotation.StringRes +import com.suwiki.feature.openmajor.R + +enum class OpenMajorTap( + val position: Int, + @StringRes val titleId: Int, +) { + ALL( + position = 0, + titleId = R.string.word_all, + ), + BOOKMARK( + position = 1, + titleId = R.string.word_bookmark, + ) +} diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/navigation/OpenMajorNavigation.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/navigation/OpenMajorNavigation.kt index 404d4f967..1e012a6db 100644 --- a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/navigation/OpenMajorNavigation.kt +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/navigation/OpenMajorNavigation.kt @@ -15,6 +15,7 @@ fun NavGraphBuilder.openMajorNavGraph( popBackStack: () -> Unit, popBackStackWithArgument: (String) -> Unit, handleException: (Throwable) -> Unit, + onShowToast: (String) -> Unit, ) { composable( route = OpenMajorRoute.route("{${OpenMajorRoute.ARGUMENT_NAME}}"), @@ -29,6 +30,7 @@ fun NavGraphBuilder.openMajorNavGraph( popBackStack = popBackStack, popBackStackWithArgument = popBackStackWithArgument, handleException = handleException, + onShowToast = onShowToast, ) } } diff --git a/feature/openmajor/src/main/res/values/strings.xml b/feature/openmajor/src/main/res/values/strings.xml index 109104213..e39f9f8c3 100644 --- a/feature/openmajor/src/main/res/values/strings.xml +++ b/feature/openmajor/src/main/res/values/strings.xml @@ -1,4 +1,7 @@ 확인 + 전체 + 즐겨찾기 + 즐겨찾기는 로그인 이후에 사용할 수 있어요. diff --git a/remote/openmajor/src/main/java/com/suwiki/remote/openmajor/api/MajorApi.kt b/remote/openmajor/src/main/java/com/suwiki/remote/openmajor/api/MajorApi.kt index a54ddca39..984bbde68 100644 --- a/remote/openmajor/src/main/java/com/suwiki/remote/openmajor/api/MajorApi.kt +++ b/remote/openmajor/src/main/java/com/suwiki/remote/openmajor/api/MajorApi.kt @@ -30,7 +30,7 @@ interface MajorApi { // 전공 즐겨찾기 하기 @POST(MAJOR) - suspend fun bookmarkMajor(@Body bookmarkMajorRequest: BookmarkMajorRequest): ApiResult + suspend fun bookmarkMajor(@Body bookmarkMajorRequest: BookmarkMajorRequest): ApiResult // 전공 즐겨찾기 조회 @GET(MAJOR) @@ -40,5 +40,5 @@ interface MajorApi { @DELETE(MAJOR) suspend fun removeBookmarkMajor( @Query(MAJOR_TYPE) majorName: String, - ): ApiResult + ): ApiResult } diff --git a/remote/openmajor/src/main/java/com/suwiki/remote/openmajor/datasource/RemoteOpenMajorDataSourceImpl.kt b/remote/openmajor/src/main/java/com/suwiki/remote/openmajor/datasource/RemoteOpenMajorDataSourceImpl.kt index 3fe5fc383..46a8a0f6d 100644 --- a/remote/openmajor/src/main/java/com/suwiki/remote/openmajor/datasource/RemoteOpenMajorDataSourceImpl.kt +++ b/remote/openmajor/src/main/java/com/suwiki/remote/openmajor/datasource/RemoteOpenMajorDataSourceImpl.kt @@ -22,10 +22,9 @@ class RemoteOpenMajorDataSourceImpl @Inject constructor( override suspend fun bookmarkMajor(majorName: String) { return majorApi.bookmarkMajor(BookmarkMajorRequest(majorType = majorName)).getOrThrow() - .run { } } override suspend fun removeBookmarkMajor(majorName: String) { - return majorApi.removeBookmarkMajor(majorName = majorName).getOrThrow().run { } + return majorApi.removeBookmarkMajor(majorName = majorName).getOrThrow() } } diff --git a/remote/openmajor/src/main/java/com/suwiki/remote/openmajor/request/BookmarkMajorRequest.kt b/remote/openmajor/src/main/java/com/suwiki/remote/openmajor/request/BookmarkMajorRequest.kt index 7b4518481..7529fadb5 100644 --- a/remote/openmajor/src/main/java/com/suwiki/remote/openmajor/request/BookmarkMajorRequest.kt +++ b/remote/openmajor/src/main/java/com/suwiki/remote/openmajor/request/BookmarkMajorRequest.kt @@ -1,3 +1,6 @@ package com.suwiki.remote.openmajor.request +import kotlinx.serialization.Serializable + +@Serializable data class BookmarkMajorRequest(val majorType: String) From c283d1fea6fac402b1bab0b0c7cdf25a53bfe8fe Mon Sep 17 00:00:00 2001 From: jinukeu Date: Fri, 29 Dec 2023 17:39:55 +0900 Subject: [PATCH 06/14] =?UTF-8?q?feat/#75:=20OpenMajor=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/repository/UserRepositoryImpl.kt | 24 ++++--- .../LectureEvaluationViewModel.kt | 1 + .../com/suwiki/feature/login/LoginScreen.kt | 12 ++-- .../feature/openmajor/OpenMajorContract.kt | 7 +- .../feature/openmajor/OpenMajorScreen.kt | 66 +++++++++++++++---- .../feature/openmajor/OpenMajorViewModel.kt | 18 ++++- .../feature/openmajor/model/OpenMajor.kt | 12 +++- .../openmajor/src/main/res/values/strings.xml | 3 + 8 files changed, 105 insertions(+), 38 deletions(-) diff --git a/data/user/src/main/java/com/suwiki/data/user/repository/UserRepositoryImpl.kt b/data/user/src/main/java/com/suwiki/data/user/repository/UserRepositoryImpl.kt index 7e072786c..f17c79d7e 100644 --- a/data/user/src/main/java/com/suwiki/data/user/repository/UserRepositoryImpl.kt +++ b/data/user/src/main/java/com/suwiki/data/user/repository/UserRepositoryImpl.kt @@ -1,5 +1,6 @@ package com.suwiki.data.user.repository +import com.suwiki.core.model.exception.AuthorizationException import com.suwiki.core.model.user.User import com.suwiki.core.security.SecurityPreferences import com.suwiki.data.user.datasource.LocalUserDataSource @@ -9,6 +10,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.zip +import timber.log.Timber import javax.inject.Inject class UserRepositoryImpl @Inject constructor( @@ -39,20 +41,16 @@ class UserRepositoryImpl @Inject constructor( val localUserInfo = localUserDataSource.user.first() emit(localUserInfo) - val remoteUserInfo = remoteUserDataSource.getUserInfo() + val remoteUserInfo = runCatching { + remoteUserDataSource.getUserInfo() + }.getOrElse { exception -> + if(exception is AuthorizationException) { + logout() + emit(User()) + return@flow + } - val isTokenExpired = with(securityPreferences) { - val (accessToken, refreshToken) = flowAccessToken().zip(flowRefreshToken()) { accessToken, refreshToken -> - (accessToken to refreshToken) - }.first() - - accessToken.isEmpty() && refreshToken.isEmpty() - } - - if (isTokenExpired) { - logout() - emit(User()) - return@flow + else throw exception } emit(remoteUserInfo) diff --git a/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationViewModel.kt b/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationViewModel.kt index b6c74c9f6..f505bdff5 100644 --- a/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationViewModel.kt +++ b/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationViewModel.kt @@ -2,6 +2,7 @@ package com.suwiki.feature.lectureevaluation.viewerreporter import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.suwiki.core.model.exception.AuthorizationException import com.suwiki.domain.user.usecase.GetUserInfoUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.catch diff --git a/feature/login/src/main/java/com/suwiki/feature/login/LoginScreen.kt b/feature/login/src/main/java/com/suwiki/feature/login/LoginScreen.kt index 7101b27e2..a341b6474 100644 --- a/feature/login/src/main/java/com/suwiki/feature/login/LoginScreen.kt +++ b/feature/login/src/main/java/com/suwiki/feature/login/LoginScreen.kt @@ -54,8 +54,8 @@ fun LoginRoute( LoginScreen( uiState = uiState, - onIdTextFieldValueChange = viewModel::updateId, - onPasswordTextFieldValueChange = viewModel::updatePassword, + onValueChangeIdTextField = viewModel::updateId, + onValueChangePasswordTextField = viewModel::updatePassword, onClickIdClearButton = { viewModel.updateId("") }, onClickPasswordClearButton = { viewModel.updatePassword("") }, onClickPasswordEyeIcon = viewModel::toggleShowPassword, @@ -71,8 +71,8 @@ fun LoginRoute( @Composable fun LoginScreen( uiState: LoginState = LoginState(), - onIdTextFieldValueChange: (String) -> Unit = {}, - onPasswordTextFieldValueChange: (String) -> Unit = {}, + onValueChangeIdTextField: (String) -> Unit = {}, + onValueChangePasswordTextField: (String) -> Unit = {}, onClickIdClearButton: () -> Unit = {}, onClickPasswordClearButton: () -> Unit = {}, onClickPasswordEyeIcon: () -> Unit = {}, @@ -97,7 +97,7 @@ fun LoginScreen( SuwikiRegularTextField( value = uiState.id, - onValueChange = onIdTextFieldValueChange, + onValueChange = onValueChangeIdTextField, onClickClearButton = onClickIdClearButton, label = stringResource(R.string.word_id), placeholder = stringResource(R.string.login_screen_id_textfield_placeholder), @@ -107,7 +107,7 @@ fun LoginScreen( SuwikiRegularTextField( value = uiState.password, - onValueChange = onPasswordTextFieldValueChange, + onValueChange = onValueChangePasswordTextField, onClickClearButton = onClickPasswordClearButton, showEyeIcon = true, showValue = uiState.showPassword, diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorContract.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorContract.kt index 2f29d8fce..2c565bcd2 100644 --- a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorContract.kt +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorContract.kt @@ -9,8 +9,13 @@ data class OpenMajorState( val filteredBookmarkedOpenMajorList: PersistentList = persistentListOf(), val showBottomShadow: Boolean = true, val currentPage: Int = 0, + val searchValue: String = "", val isLoading: Boolean = false, -) +) { + val showAllOpenMajorEmptySearchResultScreen: Boolean = searchValue.isNotEmpty() && filteredAllOpenMajorList.isEmpty() + val showBookmarkedOpenMajorSearchEmptyResultScreen: Boolean = searchValue.isNotEmpty() && filteredBookmarkedOpenMajorList.isEmpty() + val showBookmarkedOpenMajorEmptyScreen: Boolean = searchValue.isEmpty() && filteredBookmarkedOpenMajorList.isEmpty() +} sealed interface OpenMajorSideEffect { data class HandleException(val throwable: Throwable) : OpenMajorSideEffect diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt index a3a87e204..2184b863e 100644 --- a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState @@ -13,6 +14,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf @@ -24,6 +26,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -34,6 +37,7 @@ import com.suwiki.core.designsystem.component.searchbar.SuwikiSearchBar import com.suwiki.core.designsystem.component.tabbar.SuwikiTabBar import com.suwiki.core.designsystem.component.tabbar.TabTitle import com.suwiki.core.designsystem.shadow.suwikiShadow +import com.suwiki.core.designsystem.theme.Gray95 import com.suwiki.core.designsystem.theme.SuwikiTheme import com.suwiki.core.designsystem.theme.White import com.suwiki.core.ui.extension.collectWithLifecycle @@ -119,6 +123,8 @@ fun OpenMajorRoute( onClickOpenMajorContainer = viewModel::updateSelectedOpenMajor, onClickOpenMajorBookmark = viewModel::registerOrUnRegisterBookmark, onClickTab = viewModel::syncPagerState, + onValueChangeSearchBar = viewModel::updateSearchValue, + onClickSearchBarClearButton = { viewModel.updateSearchValue("") } ) } @@ -133,6 +139,8 @@ fun OpenMajorScreen( onClickConfirmButton: () -> Unit = {}, onClickOpenMajorContainer: (String) -> Unit = {}, onClickOpenMajorBookmark: (String) -> Unit = {}, + onValueChangeSearchBar: (String) -> Unit = {}, + onClickSearchBarClearButton: () -> Unit = {}, onClickTab: (Int) -> Unit = {}, ) { Box( @@ -147,7 +155,12 @@ fun OpenMajorScreen( onClickClose = onClickClose, ) - SuwikiSearchBar() + SuwikiSearchBar( + placeholder = stringResource(R.string.open_major_screen_search_bar_placeholder), + value = uiState.searchValue, + onClickClearButton = onClickSearchBarClearButton, + onValueChange = onValueChangeSearchBar, + ) SuwikiTabBar( selectedTabPosition = pagerState.currentPage, @@ -170,21 +183,32 @@ fun OpenMajorScreen( ) { page -> when (OpenMajorTap.entries[page]) { OpenMajorTap.ALL -> { - OpenMajorLazyColumn( - listState = allOpenMajorListState, - openMajorList = uiState.filteredAllOpenMajorList, - onClickOpenMajorContainer = onClickOpenMajorContainer, - onClickOpenMajorBookmark = onClickOpenMajorBookmark, - ) + if(uiState.showAllOpenMajorEmptySearchResultScreen) { + EmptyText(stringResource(R.string.open_major_empty_search_result)) + } else { + OpenMajorLazyColumn( + listState = allOpenMajorListState, + openMajorList = uiState.filteredAllOpenMajorList, + onClickOpenMajorContainer = onClickOpenMajorContainer, + onClickOpenMajorBookmark = onClickOpenMajorBookmark, + ) + } } OpenMajorTap.BOOKMARK -> { - OpenMajorLazyColumn( - listState = bookmarkedOpenMajorListState, - openMajorList = uiState.filteredBookmarkedOpenMajorList, - onClickOpenMajorContainer = onClickOpenMajorContainer, - onClickOpenMajorBookmark = onClickOpenMajorBookmark, - ) + if(uiState.showAllOpenMajorEmptySearchResultScreen) { + EmptyText(stringResource(R.string.open_major_empty_search_result)) + } else if(uiState.showBookmarkedOpenMajorEmptyScreen) { + EmptyText(stringResource(R.string.open_major_empty_bookmark)) + } + else { + OpenMajorLazyColumn( + listState = bookmarkedOpenMajorListState, + openMajorList = uiState.filteredBookmarkedOpenMajorList, + onClickOpenMajorContainer = onClickOpenMajorContainer, + onClickOpenMajorBookmark = onClickOpenMajorBookmark, + ) + } } } } @@ -194,6 +218,7 @@ fun OpenMajorScreen( ) { SuwikiContainedLargeButton( modifier = Modifier + .imePadding() .suwikiShadow( color = if (uiState.showBottomShadow) White else Color.Transparent, blurRadius = 80.dp, @@ -209,6 +234,21 @@ fun OpenMajorScreen( } } +@Composable +private fun EmptyText( + text: String = "", +) { + Text( + modifier = Modifier + .padding(52.dp) + .fillMaxSize(), + textAlign = TextAlign.Center, + text = text, + style = SuwikiTheme.typography.header4, + color = Gray95, + ) +} + @Composable private fun OpenMajorLazyColumn( listState: LazyListState, diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt index b07b184bf..e0c14ee26 100644 --- a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt @@ -21,6 +21,8 @@ import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.annotation.OrbitExperimental +import org.orbitmvi.orbit.syntax.simple.blockingIntent import org.orbitmvi.orbit.syntax.simple.intent import org.orbitmvi.orbit.syntax.simple.postSideEffect import org.orbitmvi.orbit.syntax.simple.reduce @@ -42,6 +44,13 @@ class OpenMajorViewModel @Inject constructor( private val allOpenMajorList = mutableListOf() private val bookmarkedOpenMajorList = mutableListOf() + @OptIn(OrbitExperimental::class) + fun updateSearchValue(searchValue: String) = blockingIntent { + reduce { state.copy(searchValue = searchValue) } + reduceOpenMajorList(searchValue) + reduceBookmarkedOpenMajorList(searchValue) + } + fun registerOrUnRegisterBookmark(openMajor: String) = intent { if (isLoggedIn.not()) { postSideEffect(OpenMajorSideEffect.ShowNeedLoginToast) @@ -117,10 +126,11 @@ class OpenMajorViewModel @Inject constructor( }.launchIn(viewModelScope) } - private fun reduceOpenMajorList() = intent { + private fun reduceOpenMajorList(searchValue: String = container.stateFlow.value.searchValue) = intent { reduce { state.copy( filteredAllOpenMajorList = allOpenMajorList.toOpenMajorList( + searchValue = searchValue, bookmarkedOpenMajorList = bookmarkedOpenMajorList, selectedOpenMajor = selectedOpenMajor, ), @@ -141,10 +151,12 @@ class OpenMajorViewModel @Inject constructor( } } - private fun reduceBookmarkedOpenMajorList() = intent { + private fun reduceBookmarkedOpenMajorList(searchValue: String = container.stateFlow.value.searchValue) = intent { reduce { state.copy( - filteredBookmarkedOpenMajorList = bookmarkedOpenMajorList.toBookmarkedOpenMajorList(selectedOpenMajor), + filteredBookmarkedOpenMajorList = bookmarkedOpenMajorList.toBookmarkedOpenMajorList( + searchValue = searchValue, selectedOpenMajor = selectedOpenMajor + ), ) } } diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/model/OpenMajor.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/model/OpenMajor.kt index 0de396027..3a80d19b6 100644 --- a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/model/OpenMajor.kt +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/model/OpenMajor.kt @@ -11,8 +11,12 @@ data class OpenMajor( ) fun List.toBookmarkedOpenMajorList( + searchValue: String, selectedOpenMajor: String, -) = map { name -> +) = filter { openMajor -> + if (searchValue.isNotEmpty()) searchValue in openMajor + else true +}.map { name -> OpenMajor( name = name, isBookmarked = true, @@ -21,9 +25,13 @@ fun List.toBookmarkedOpenMajorList( }.toPersistentList() fun List.toOpenMajorList( + searchValue: String, bookmarkedOpenMajorList: List, selectedOpenMajor: String, -) = map { name -> +) = filter { openMajor -> + if (searchValue.isNotEmpty()) searchValue in openMajor + else true +}.map { name -> OpenMajor( name = name, isBookmarked = name in bookmarkedOpenMajorList, diff --git a/feature/openmajor/src/main/res/values/strings.xml b/feature/openmajor/src/main/res/values/strings.xml index e39f9f8c3..dd85b0492 100644 --- a/feature/openmajor/src/main/res/values/strings.xml +++ b/feature/openmajor/src/main/res/values/strings.xml @@ -4,4 +4,7 @@ 전체 즐겨찾기 즐겨찾기는 로그인 이후에 사용할 수 있어요. + 개설학과를 검색하세요 + 검색 결과가 없습니다.\n검색어가 정확한지 확인하세요. + 등록된 즐겨찾기가 없어요. From bd08bdcd8b7d86559ac49d5d6c83cc5da7f9d5a8 Mon Sep 17 00:00:00 2001 From: jinukeu Date: Fri, 29 Dec 2023 17:40:56 +0900 Subject: [PATCH 07/14] chore/#75: ktlint format --- .../component/tabbar/SuwikiTabBar.kt | 11 +++++++---- .../repository/OpenMajorRepositoryImpl.kt | 1 - .../data/user/repository/UserRepositoryImpl.kt | 8 +++----- .../viewerreporter/LectureEvaluationViewModel.kt | 1 - .../navigation/LectureEvaluationNavigation.kt | 1 - .../com/suwiki/feature/navigator/MainScreen.kt | 1 - .../suwiki/feature/navigator/MainViewModel.kt | 14 +++++--------- .../suwiki/feature/openmajor/OpenMajorScreen.kt | 16 ++++++---------- .../feature/openmajor/OpenMajorViewModel.kt | 12 +++++++----- .../suwiki/feature/openmajor/model/OpenMajor.kt | 14 ++++++++++---- .../feature/openmajor/model/OpenMajorTap.kt | 2 +- 11 files changed, 39 insertions(+), 42 deletions(-) diff --git a/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/tabbar/SuwikiTabBar.kt b/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/tabbar/SuwikiTabBar.kt index 417a28ede..a47936a00 100644 --- a/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/tabbar/SuwikiTabBar.kt +++ b/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/tabbar/SuwikiTabBar.kt @@ -42,7 +42,7 @@ import com.suwiki.core.ui.extension.suwikiClickable enum class SubComposeID { PRE_CALCULATE_ITEM, ITEM, - INDICATOR + INDICATOR, } data class TabPosition( @@ -117,11 +117,13 @@ private fun Modifier.tabIndicator( ) { val currentTabWidth by animateDpAsState( targetValue = tabPosition.width, - animationSpec = animationSpec, label = "", + animationSpec = animationSpec, + label = "", ) val indicatorOffset by animateDpAsState( targetValue = tabPosition.left, - animationSpec = animationSpec, label = "", + animationSpec = animationSpec, + label = "", ) fillMaxWidth() .wrapContentSize(Alignment.BottomStart) @@ -158,7 +160,8 @@ fun SuwikiTabBarPreview() { var selectedTabPosition by remember { mutableIntStateOf(0) } val items = listOf( - "메뉴(0)", "메뉴(0)", + "메뉴(0)", + "메뉴(0)", ) SuwikiTabBar( diff --git a/data/openmajor/src/main/java/com/suwiki/data/openmajor/repository/OpenMajorRepositoryImpl.kt b/data/openmajor/src/main/java/com/suwiki/data/openmajor/repository/OpenMajorRepositoryImpl.kt index 72b5994ea..5c50c706f 100644 --- a/data/openmajor/src/main/java/com/suwiki/data/openmajor/repository/OpenMajorRepositoryImpl.kt +++ b/data/openmajor/src/main/java/com/suwiki/data/openmajor/repository/OpenMajorRepositoryImpl.kt @@ -4,7 +4,6 @@ import com.suwiki.core.model.openmajor.OpenMajor import com.suwiki.data.openmajor.datasource.LocalOpenMajorDataSource import com.suwiki.data.openmajor.datasource.RemoteOpenMajorDataSource import com.suwiki.domain.openmajor.repository.OpenMajorRepository -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flow diff --git a/data/user/src/main/java/com/suwiki/data/user/repository/UserRepositoryImpl.kt b/data/user/src/main/java/com/suwiki/data/user/repository/UserRepositoryImpl.kt index f17c79d7e..5bcd8445e 100644 --- a/data/user/src/main/java/com/suwiki/data/user/repository/UserRepositoryImpl.kt +++ b/data/user/src/main/java/com/suwiki/data/user/repository/UserRepositoryImpl.kt @@ -9,8 +9,6 @@ import com.suwiki.domain.user.repository.UserRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.zip -import timber.log.Timber import javax.inject.Inject class UserRepositoryImpl @Inject constructor( @@ -44,13 +42,13 @@ class UserRepositoryImpl @Inject constructor( val remoteUserInfo = runCatching { remoteUserDataSource.getUserInfo() }.getOrElse { exception -> - if(exception is AuthorizationException) { + if (exception is AuthorizationException) { logout() emit(User()) return@flow + } else { + throw exception } - - else throw exception } emit(remoteUserInfo) diff --git a/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationViewModel.kt b/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationViewModel.kt index f505bdff5..b6c74c9f6 100644 --- a/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationViewModel.kt +++ b/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationViewModel.kt @@ -2,7 +2,6 @@ package com.suwiki.feature.lectureevaluation.viewerreporter import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.suwiki.core.model.exception.AuthorizationException import com.suwiki.domain.user.usecase.GetUserInfoUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.catch diff --git a/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/navigation/LectureEvaluationNavigation.kt b/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/navigation/LectureEvaluationNavigation.kt index 2edd0b164..db065f743 100644 --- a/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/navigation/LectureEvaluationNavigation.kt +++ b/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/navigation/LectureEvaluationNavigation.kt @@ -6,7 +6,6 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.suwiki.feature.lectureevaluation.viewerreporter.LectureEvaluationRoute -import timber.log.Timber fun NavController.navigateLectureEvaluation(navOptions: NavOptions) { navigate(LectureEvaluationRoute.route, navOptions) diff --git a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt index a591e51f9..f456a2fe6 100644 --- a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt +++ b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt @@ -18,7 +18,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue diff --git a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainViewModel.kt b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainViewModel.kt index e7cd6b1b5..73b9aaa4f 100644 --- a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainViewModel.kt +++ b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainViewModel.kt @@ -1,13 +1,11 @@ package com.suwiki.feature.navigator import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.suwiki.core.android.recordException import com.suwiki.core.model.exception.NetworkException import com.suwiki.core.model.exception.UnknownException import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.orbitmvi.orbit.Container @@ -25,13 +23,11 @@ class MainViewModel @Inject constructor() : ContainerHost when (OpenMajorTap.entries[page]) { OpenMajorTap.ALL -> { - if(uiState.showAllOpenMajorEmptySearchResultScreen) { + if (uiState.showAllOpenMajorEmptySearchResultScreen) { EmptyText(stringResource(R.string.open_major_empty_search_result)) } else { OpenMajorLazyColumn( @@ -196,12 +193,11 @@ fun OpenMajorScreen( } OpenMajorTap.BOOKMARK -> { - if(uiState.showAllOpenMajorEmptySearchResultScreen) { + if (uiState.showAllOpenMajorEmptySearchResultScreen) { EmptyText(stringResource(R.string.open_major_empty_search_result)) - } else if(uiState.showBookmarkedOpenMajorEmptyScreen) { + } else if (uiState.showBookmarkedOpenMajorEmptyScreen) { EmptyText(stringResource(R.string.open_major_empty_bookmark)) - } - else { + } else { OpenMajorLazyColumn( listState = bookmarkedOpenMajorListState, openMajorList = uiState.filteredBookmarkedOpenMajorList, @@ -271,7 +267,7 @@ private fun OpenMajorLazyColumn( isChecked = isSelected, isStared = isBookmarked, onClick = { onClickOpenMajorContainer(name) }, - onClickStar = { onClickOpenMajorBookmark(name) } + onClickStar = { onClickOpenMajorBookmark(name) }, ) } } diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt index e0c14ee26..d45f1f7e6 100644 --- a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt @@ -13,12 +13,10 @@ import com.suwiki.feature.openmajor.model.toBookmarkedOpenMajorList import com.suwiki.feature.openmajor.model.toOpenMajorList import com.suwiki.feature.openmajor.navigation.OpenMajorRoute import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.joinAll -import kotlinx.coroutines.launch import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.annotation.OrbitExperimental @@ -146,8 +144,11 @@ class OpenMajorViewModel @Inject constructor( reduceOpenMajorList() } .onFailure { - if (it is AuthorizationException) isLoggedIn = false - else postSideEffect(OpenMajorSideEffect.HandleException(it)) + if (it is AuthorizationException) { + isLoggedIn = false + } else { + postSideEffect(OpenMajorSideEffect.HandleException(it)) + } } } @@ -155,7 +156,8 @@ class OpenMajorViewModel @Inject constructor( reduce { state.copy( filteredBookmarkedOpenMajorList = bookmarkedOpenMajorList.toBookmarkedOpenMajorList( - searchValue = searchValue, selectedOpenMajor = selectedOpenMajor + searchValue = searchValue, + selectedOpenMajor = selectedOpenMajor, ), ) } diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/model/OpenMajor.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/model/OpenMajor.kt index 3a80d19b6..9497711ab 100644 --- a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/model/OpenMajor.kt +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/model/OpenMajor.kt @@ -14,8 +14,11 @@ fun List.toBookmarkedOpenMajorList( searchValue: String, selectedOpenMajor: String, ) = filter { openMajor -> - if (searchValue.isNotEmpty()) searchValue in openMajor - else true + if (searchValue.isNotEmpty()) { + searchValue in openMajor + } else { + true + } }.map { name -> OpenMajor( name = name, @@ -29,8 +32,11 @@ fun List.toOpenMajorList( bookmarkedOpenMajorList: List, selectedOpenMajor: String, ) = filter { openMajor -> - if (searchValue.isNotEmpty()) searchValue in openMajor - else true + if (searchValue.isNotEmpty()) { + searchValue in openMajor + } else { + true + } }.map { name -> OpenMajor( name = name, diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/model/OpenMajorTap.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/model/OpenMajorTap.kt index f3c03b2de..6b0368c2e 100644 --- a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/model/OpenMajorTap.kt +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/model/OpenMajorTap.kt @@ -14,5 +14,5 @@ enum class OpenMajorTap( BOOKMARK( position = 1, titleId = R.string.word_bookmark, - ) + ), } From fcb2fcc920819a4339c798ecf61667ee6f3fe340 Mon Sep 17 00:00:00 2001 From: jinukeu Date: Fri, 29 Dec 2023 17:44:46 +0900 Subject: [PATCH 08/14] rename/#75: registerOrUnRegisterBookmark -> toggleBookmark --- .../main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt | 2 +- .../java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt index ea90280d3..cb5f4e3be 100644 --- a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt @@ -118,7 +118,7 @@ fun OpenMajorRoute( onClickClose = viewModel::popBackStack, onClickConfirmButton = viewModel::popBackStackWithArgument, onClickOpenMajorContainer = viewModel::updateSelectedOpenMajor, - onClickOpenMajorBookmark = viewModel::registerOrUnRegisterBookmark, + onClickOpenMajorBookmark = viewModel::toggleBookmark, onClickTab = viewModel::syncPagerState, onValueChangeSearchBar = viewModel::updateSearchValue, onClickSearchBarClearButton = { viewModel.updateSearchValue("") }, diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt index d45f1f7e6..89f48660b 100644 --- a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorViewModel.kt @@ -49,7 +49,7 @@ class OpenMajorViewModel @Inject constructor( reduceBookmarkedOpenMajorList(searchValue) } - fun registerOrUnRegisterBookmark(openMajor: String) = intent { + fun toggleBookmark(openMajor: String) = intent { if (isLoggedIn.not()) { postSideEffect(OpenMajorSideEffect.ShowNeedLoginToast) return@intent From e06157b27370fa4f9bf31c3675c7bb884f9b6033 Mon Sep 17 00:00:00 2001 From: jinukeu Date: Fri, 29 Dec 2023 17:49:51 +0900 Subject: [PATCH 09/14] =?UTF-8?q?fix/#75:=20=EA=B2=80=EC=83=89=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=20=EC=97=86=EC=9D=8C=20=EC=8A=A4=ED=81=AC=EB=A6=B0?= =?UTF-8?q?=EC=9D=B4=20=EC=A0=9C=EB=8C=80=EB=A1=9C=20=ED=91=9C=EC=8B=9C?= =?UTF-8?q?=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt index cb5f4e3be..a81710bac 100644 --- a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt @@ -193,7 +193,7 @@ fun OpenMajorScreen( } OpenMajorTap.BOOKMARK -> { - if (uiState.showAllOpenMajorEmptySearchResultScreen) { + if (uiState.showBookmarkedOpenMajorSearchEmptyResultScreen) { EmptyText(stringResource(R.string.open_major_empty_search_result)) } else if (uiState.showBookmarkedOpenMajorEmptyScreen) { EmptyText(stringResource(R.string.open_major_empty_bookmark)) From 523631214b8ee90a971debbb5b3acca8cbc86f7c Mon Sep 17 00:00:00 2001 From: jinukeu Date: Fri, 29 Dec 2023 21:38:10 +0900 Subject: [PATCH 10/14] =?UTF-8?q?refactor/#75:=20SuwikiAppBarWithTitle,=20?= =?UTF-8?q?=ED=83=80=EC=9D=B4=ED=8B=80,=20closeIcon=20show=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/appbar/SuwikiAppBarWithTitle.kt | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/appbar/SuwikiAppBarWithTitle.kt b/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/appbar/SuwikiAppBarWithTitle.kt index d9245b626..f343ea78a 100644 --- a/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/appbar/SuwikiAppBarWithTitle.kt +++ b/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/appbar/SuwikiAppBarWithTitle.kt @@ -25,7 +25,8 @@ import com.suwiki.core.ui.extension.suwikiClickable @Composable fun SuwikiAppBarWithTitle( modifier: Modifier = Modifier, - title: String = "", + title: String? = null, + showCloseIcon: Boolean = true, showBackIcon: Boolean = true, onClickBack: () -> Unit = {}, onClickClose: () -> Unit = {}, @@ -50,22 +51,28 @@ fun SuwikiAppBarWithTitle( tint = Gray95, ) } - Text( - modifier = Modifier.align(Alignment.Center), - text = title, - style = SuwikiTheme.typography.header6, - ) - Icon( - modifier = Modifier - .align(Alignment.CenterEnd) - .size(24.dp) - .clip(CircleShape) - .suwikiClickable(onClick = onClickClose) - .padding(3.dp), - painter = painterResource(id = R.drawable.ic_appbar_close_mark), - contentDescription = "", - tint = Gray95, - ) + + if(title != null) { + Text( + modifier = Modifier.align(Alignment.Center), + text = title, + style = SuwikiTheme.typography.header6, + ) + } + + if (showCloseIcon) { + Icon( + modifier = Modifier + .align(Alignment.CenterEnd) + .size(24.dp) + .clip(CircleShape) + .suwikiClickable(onClick = onClickClose) + .padding(3.dp), + painter = painterResource(id = R.drawable.ic_appbar_close_mark), + contentDescription = "", + tint = Gray95, + ) + } } } From 0a7462ed08abcce7b9d9fb73160a218139d98736 Mon Sep 17 00:00:00 2001 From: jinukeu Date: Fri, 29 Dec 2023 22:03:01 +0900 Subject: [PATCH 11/14] chore/#75: ktlint format --- .../core/designsystem/component/appbar/SuwikiAppBarWithTitle.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/appbar/SuwikiAppBarWithTitle.kt b/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/appbar/SuwikiAppBarWithTitle.kt index f343ea78a..d88e32763 100644 --- a/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/appbar/SuwikiAppBarWithTitle.kt +++ b/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/appbar/SuwikiAppBarWithTitle.kt @@ -52,7 +52,7 @@ fun SuwikiAppBarWithTitle( ) } - if(title != null) { + if (title != null) { Text( modifier = Modifier.align(Alignment.Center), text = title, From 86f5db1fa7df093b244dbadc026aad7e7157fc7b Mon Sep 17 00:00:00 2001 From: jinukeu Date: Sun, 31 Dec 2023 10:33:41 +0900 Subject: [PATCH 12/14] =?UTF-8?q?refactor/#73:=20=EA=B3=B5=EC=A7=80?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../suwiki/core/model/notice/NoticeDetail.kt | 2 ++ .../suwiki/feature/navigator/MainNavigator.kt | 4 +-- .../suwiki/feature/navigator/MainScreen.kt | 1 + .../suwiki/feature/notice/NoticeContract.kt | 3 +- .../com/suwiki/feature/notice/NoticeScreen.kt | 14 ++++---- .../suwiki/feature/notice/NoticeViewModel.kt | 22 ++++++------- .../{ => detail}/NoticeDetailContract.kt | 6 ++-- .../notice/{ => detail}/NoticeDetailScreen.kt | 6 ++-- .../{ => detail}/NoticeDetailViewModel.kt | 32 ++++++++++--------- .../notice/navigation/NoticeNavigation.kt | 27 +++++++++++----- .../com/suwiki/remote/notice/api/NoticeApi.kt | 2 +- 11 files changed, 69 insertions(+), 50 deletions(-) rename feature/notice/src/main/java/com/suwiki/feature/notice/{ => detail}/NoticeDetailContract.kt (60%) rename feature/notice/src/main/java/com/suwiki/feature/notice/{ => detail}/NoticeDetailScreen.kt (94%) rename feature/notice/src/main/java/com/suwiki/feature/notice/{ => detail}/NoticeDetailViewModel.kt (62%) diff --git a/core/model/src/main/java/com/suwiki/core/model/notice/NoticeDetail.kt b/core/model/src/main/java/com/suwiki/core/model/notice/NoticeDetail.kt index e70c156c4..0e490eb7b 100644 --- a/core/model/src/main/java/com/suwiki/core/model/notice/NoticeDetail.kt +++ b/core/model/src/main/java/com/suwiki/core/model/notice/NoticeDetail.kt @@ -1,7 +1,9 @@ package com.suwiki.core.model.notice +import androidx.compose.runtime.Stable import java.time.LocalDateTime +@Stable data class NoticeDetail( val id: Long = 0, val title: String = "", diff --git a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainNavigator.kt b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainNavigator.kt index 28c584a86..0e654c636 100644 --- a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainNavigator.kt +++ b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainNavigator.kt @@ -55,8 +55,8 @@ internal class MainNavigator( navController.navigateNotice() } - fun navigateNoticeDetail() { - navController.navigateNoticeDetail() + fun navigateNoticeDetail(noticeId: Long) { + navController.navigateNoticeDetail(noticeId) } fun navigateOpenMajor(selectedOpenMajor: String) { diff --git a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt index 0bdc1fd82..260f492a3 100644 --- a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt +++ b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt @@ -106,6 +106,7 @@ internal fun MainScreen( padding = innerPadding, popBackStack = navigator::popBackStackIfNotHome, navigateNoticeDetail = navigator::navigateNoticeDetail, + handleException = viewModel::handleException, ) } diff --git a/feature/notice/src/main/java/com/suwiki/feature/notice/NoticeContract.kt b/feature/notice/src/main/java/com/suwiki/feature/notice/NoticeContract.kt index aa2edf332..2c1438b69 100644 --- a/feature/notice/src/main/java/com/suwiki/feature/notice/NoticeContract.kt +++ b/feature/notice/src/main/java/com/suwiki/feature/notice/NoticeContract.kt @@ -10,6 +10,7 @@ data class NoticeState( ) sealed interface NoticeSideEffect { - data object NavigateNoticeDetail : NoticeSideEffect + data class NavigateNoticeDetail(val noticeId: Long) : NoticeSideEffect data object PopBackStack : NoticeSideEffect + data class HandleException(val exception: Throwable) : NoticeSideEffect } diff --git a/feature/notice/src/main/java/com/suwiki/feature/notice/NoticeScreen.kt b/feature/notice/src/main/java/com/suwiki/feature/notice/NoticeScreen.kt index 5391430f7..045678e2c 100644 --- a/feature/notice/src/main/java/com/suwiki/feature/notice/NoticeScreen.kt +++ b/feature/notice/src/main/java/com/suwiki/feature/notice/NoticeScreen.kt @@ -31,14 +31,16 @@ import java.time.LocalDateTime fun NoticeRoute( padding: PaddingValues, viewModel: NoticeViewModel = hiltViewModel(), - navigateNoticeDetail: () -> Unit, + navigateNoticeDetail: (Long) -> Unit, popBackStack: () -> Unit, + handleException: (Throwable) -> Unit, ) { val uiState = viewModel.collectAsState().value viewModel.collectSideEffect { sideEffect -> when (sideEffect) { - NoticeSideEffect.NavigateNoticeDetail -> navigateNoticeDetail() + is NoticeSideEffect.NavigateNoticeDetail -> navigateNoticeDetail(sideEffect.noticeId) NoticeSideEffect.PopBackStack -> popBackStack() + is NoticeSideEffect.HandleException -> handleException(sideEffect.exception) } } @@ -50,7 +52,7 @@ fun NoticeRoute( padding = padding, noticeList = uiState.noticeList, uiState = uiState, - navigateNoticeDetail = { viewModel.navigateNoticeDetail() }, + navigateNoticeDetail = viewModel::navigateNoticeDetail, popBackStack = viewModel::popBackStack, ) } @@ -61,7 +63,7 @@ fun NoticeScreen( padding: PaddingValues, noticeList: PersistentList, uiState: NoticeState, - navigateNoticeDetail: () -> Unit, + navigateNoticeDetail: (Long) -> Unit, popBackStack: () -> Unit, ) { Column( @@ -77,11 +79,11 @@ fun NoticeScreen( showCloseIcon = false, ) LazyColumn { - items(items = noticeList) { notice -> + items(items = noticeList, key = { it.id }) { notice -> SuwikiNoticeContainer( titleText = notice.title, dateText = notice.date.toString(), - onClick = navigateNoticeDetail, + onClick = { navigateNoticeDetail(notice.id) }, ) } } diff --git a/feature/notice/src/main/java/com/suwiki/feature/notice/NoticeViewModel.kt b/feature/notice/src/main/java/com/suwiki/feature/notice/NoticeViewModel.kt index 5d25ff3ee..3c72b368f 100644 --- a/feature/notice/src/main/java/com/suwiki/feature/notice/NoticeViewModel.kt +++ b/feature/notice/src/main/java/com/suwiki/feature/notice/NoticeViewModel.kt @@ -23,22 +23,20 @@ class NoticeViewModel @Inject constructor( override val container: Container = container(NoticeState()) - suspend fun loadNoticeList() { + fun loadNoticeList() = intent { showLoadingScreen() - viewModelScope.launch { - getNoticeListUseCase(1) - .onSuccess { notices -> - intent { reduce { state.copy(noticeList = notices.toPersistentList()) } } - hideLoadingScreen() - } - .onFailure { - intent { reduce { state.copy(noticeList = persistentListOf(Notice())) } } - } - } + getNoticeListUseCase(1) + .onSuccess { notices -> + reduce { state.copy(noticeList = notices.toPersistentList()) } + } + .onFailure { + postSideEffect(NoticeSideEffect.HandleException(it)) + } + hideLoadingScreen() } private fun showLoadingScreen() = intent { reduce { state.copy(isLoading = true) } } private fun hideLoadingScreen() = intent { reduce { state.copy(isLoading = false) } } - fun navigateNoticeDetail() = intent { postSideEffect(NoticeSideEffect.NavigateNoticeDetail) } + fun navigateNoticeDetail(noticeId: Long) = intent { postSideEffect(NoticeSideEffect.NavigateNoticeDetail(noticeId)) } fun popBackStack() = intent { postSideEffect(NoticeSideEffect.PopBackStack) } } diff --git a/feature/notice/src/main/java/com/suwiki/feature/notice/NoticeDetailContract.kt b/feature/notice/src/main/java/com/suwiki/feature/notice/detail/NoticeDetailContract.kt similarity index 60% rename from feature/notice/src/main/java/com/suwiki/feature/notice/NoticeDetailContract.kt rename to feature/notice/src/main/java/com/suwiki/feature/notice/detail/NoticeDetailContract.kt index c04ed0650..6753e2dae 100644 --- a/feature/notice/src/main/java/com/suwiki/feature/notice/NoticeDetailContract.kt +++ b/feature/notice/src/main/java/com/suwiki/feature/notice/detail/NoticeDetailContract.kt @@ -1,14 +1,14 @@ -package com.suwiki.feature.notice +package com.suwiki.feature.notice.detail -import androidx.compose.runtime.Stable import com.suwiki.core.model.notice.NoticeDetail +import com.suwiki.feature.notice.NoticeSideEffect data class NoticeDetailState( val isLoading: Boolean = false, - @Stable val noticeDetail: NoticeDetail = NoticeDetail(), ) sealed interface NoticeDetailSideEffect { data object PopBackStack : NoticeDetailSideEffect + data class HandleException(val exception: Throwable) : NoticeDetailSideEffect } diff --git a/feature/notice/src/main/java/com/suwiki/feature/notice/NoticeDetailScreen.kt b/feature/notice/src/main/java/com/suwiki/feature/notice/detail/NoticeDetailScreen.kt similarity index 94% rename from feature/notice/src/main/java/com/suwiki/feature/notice/NoticeDetailScreen.kt rename to feature/notice/src/main/java/com/suwiki/feature/notice/detail/NoticeDetailScreen.kt index 8730eed5f..fa55509c0 100644 --- a/feature/notice/src/main/java/com/suwiki/feature/notice/NoticeDetailScreen.kt +++ b/feature/notice/src/main/java/com/suwiki/feature/notice/detail/NoticeDetailScreen.kt @@ -1,4 +1,4 @@ -package com.suwiki.feature.notice +package com.suwiki.feature.notice.detail import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column @@ -33,11 +33,13 @@ fun NoticeDetailRoute( padding: PaddingValues, viewModel: NoticeDetailViewModel = hiltViewModel(), popBackStack: () -> Unit = {}, + handleException: (Throwable) -> Unit, ) { val uiState = viewModel.collectAsState().value viewModel.collectSideEffect { sideEffect -> when (sideEffect) { NoticeDetailSideEffect.PopBackStack -> popBackStack() + is NoticeDetailSideEffect.HandleException -> handleException(sideEffect.exception) } } @@ -74,7 +76,7 @@ fun NoticeDetailScreen( Column { NoticeDetailTitleContainer( title = uiState.noticeDetail.title, - date = uiState.noticeDetail.date.toString(), + date = uiState.noticeDetail.date?.toString() ?: "", ) HorizontalDivider( thickness = 4.dp, diff --git a/feature/notice/src/main/java/com/suwiki/feature/notice/NoticeDetailViewModel.kt b/feature/notice/src/main/java/com/suwiki/feature/notice/detail/NoticeDetailViewModel.kt similarity index 62% rename from feature/notice/src/main/java/com/suwiki/feature/notice/NoticeDetailViewModel.kt rename to feature/notice/src/main/java/com/suwiki/feature/notice/detail/NoticeDetailViewModel.kt index 68fcad6c9..d501719c4 100644 --- a/feature/notice/src/main/java/com/suwiki/feature/notice/NoticeDetailViewModel.kt +++ b/feature/notice/src/main/java/com/suwiki/feature/notice/detail/NoticeDetailViewModel.kt @@ -1,11 +1,12 @@ -package com.suwiki.feature.notice +package com.suwiki.feature.notice.detail +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope +import com.suwiki.core.model.exception.UnknownException import com.suwiki.core.model.notice.NoticeDetail import com.suwiki.domain.notice.usecase.GetNoticeDetailUseCase +import com.suwiki.feature.notice.navigation.NoticeRoute import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.syntax.simple.intent @@ -17,21 +18,22 @@ import javax.inject.Inject @HiltViewModel class NoticeDetailViewModel @Inject constructor( private val getNoticeDetailUseCase: GetNoticeDetailUseCase, + savedStateHandle: SavedStateHandle, ) : ContainerHost, ViewModel() { override val container: Container = container(NoticeDetailState()) - suspend fun loadNoticeDetail() { - viewModelScope.launch { - showLoadingScreen() - getNoticeDetailUseCase(1) - .onSuccess { noticeDetail -> - intent { reduce { state.copy(noticeDetail = noticeDetail) } } - hideLoadingScreen() - } - .onFailure { - intent { reduce { state.copy(noticeDetail = NoticeDetail()) } } - } - } + private val noticeId: Long = savedStateHandle.get(NoticeRoute.DETAIL_ARGUMENT_NAME)!!.toLong() + + fun loadNoticeDetail() = intent { + showLoadingScreen() + getNoticeDetailUseCase(noticeId) + .onSuccess { noticeDetail -> + reduce { state.copy(noticeDetail = noticeDetail) } + } + .onFailure { + postSideEffect(NoticeDetailSideEffect.HandleException(it)) + } + hideLoadingScreen() } private fun showLoadingScreen() = intent { reduce { state.copy(isLoading = true) } } diff --git a/feature/notice/src/main/java/com/suwiki/feature/notice/navigation/NoticeNavigation.kt b/feature/notice/src/main/java/com/suwiki/feature/notice/navigation/NoticeNavigation.kt index 8ea6b0d04..0987380d1 100644 --- a/feature/notice/src/main/java/com/suwiki/feature/notice/navigation/NoticeNavigation.kt +++ b/feature/notice/src/main/java/com/suwiki/feature/notice/navigation/NoticeNavigation.kt @@ -3,42 +3,53 @@ package com.suwiki.feature.notice.navigation import androidx.compose.foundation.layout.PaddingValues import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType import androidx.navigation.compose.composable -import com.suwiki.feature.notice.NoticeDetailRoute +import androidx.navigation.navArgument +import com.suwiki.feature.notice.detail.NoticeDetailRoute import com.suwiki.feature.notice.NoticeRoute fun NavController.navigateNotice() { navigate(NoticeRoute.route) } -fun NavController.navigateNoticeDetail() { - navigate(NoticeDetailRoute.route) +fun NavController.navigateNoticeDetail(noticeId: Long) { + navigate(NoticeRoute.detailRoute(noticeId.toString())) } fun NavGraphBuilder.noticeNavGraph( padding: PaddingValues, popBackStack: () -> Unit = {}, - navigateNoticeDetail: () -> Unit = {}, + navigateNoticeDetail: (Long) -> Unit = {}, + handleException: (Throwable) -> Unit, ) { composable(route = NoticeRoute.route) { NoticeRoute( padding = padding, navigateNoticeDetail = navigateNoticeDetail, popBackStack = popBackStack, + handleException = handleException, ) } - composable(route = NoticeDetailRoute.route) { + composable( + route = NoticeRoute.detailRoute("{${NoticeRoute.DETAIL_ARGUMENT_NAME}}"), + arguments = listOf( + navArgument(NoticeRoute.DETAIL_ARGUMENT_NAME) { + type = NavType.StringType + }, + ), + ) { NoticeDetailRoute( padding = padding, popBackStack = popBackStack, + handleException = handleException, ) } } object NoticeRoute { const val route = "notice" -} + const val DETAIL_ARGUMENT_NAME = "noticeId" -object NoticeDetailRoute { - const val route = "notice-detail" + fun detailRoute(noticeId: String) = "notice/detail/$noticeId" } diff --git a/remote/notice/src/main/java/com/suwiki/remote/notice/api/NoticeApi.kt b/remote/notice/src/main/java/com/suwiki/remote/notice/api/NoticeApi.kt index 4c3602c51..20b6e0be3 100644 --- a/remote/notice/src/main/java/com/suwiki/remote/notice/api/NoticeApi.kt +++ b/remote/notice/src/main/java/com/suwiki/remote/notice/api/NoticeApi.kt @@ -21,7 +21,7 @@ interface NoticeApi { ): ApiResult>> // 공지사항 API - @GET(NOTICE) + @GET("$NOTICE/") suspend fun getNotice( @Query(QUERY_NOTICE_ID) id: Long, ): ApiResult> From 12bb7f82ad9c2a72bcf606bb3b8201a4d9c76688 Mon Sep 17 00:00:00 2001 From: jinukeu Date: Sun, 31 Dec 2023 10:37:09 +0900 Subject: [PATCH 13/14] =?UTF-8?q?refactor/#75:=20OpenMajor=20LazyColumn=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/suwiki/feature/openmajor/OpenMajorScreen.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt index a81710bac..008589802 100644 --- a/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt +++ b/feature/openmajor/src/main/java/com/suwiki/feature/openmajor/OpenMajorScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState @@ -258,10 +259,10 @@ private fun OpenMajorLazyColumn( contentPadding = PaddingValues(top = 12.dp), ) { items( - count = openMajorList.size, - key = { index -> openMajorList[index].id }, - ) { index -> - with(openMajorList[index]) { + items = openMajorList, + key = { it.id }, + ) { openMajor -> + with(openMajor) { OpenMajorContainer( text = name, isChecked = isSelected, From 24789d5a366dca78c03c41accf1727c9a7cdf072 Mon Sep 17 00:00:00 2001 From: jinukeu Date: Sun, 31 Dec 2023 10:37:49 +0900 Subject: [PATCH 14/14] chore/#75: ktlint --- .../main/java/com/suwiki/feature/navigator/MainNavigator.kt | 2 +- .../src/main/java/com/suwiki/feature/navigator/MainScreen.kt | 2 +- .../main/java/com/suwiki/feature/notice/NoticeViewModel.kt | 4 ---- .../com/suwiki/feature/notice/detail/NoticeDetailContract.kt | 1 - .../com/suwiki/feature/notice/detail/NoticeDetailViewModel.kt | 2 -- .../com/suwiki/feature/notice/navigation/NoticeNavigation.kt | 2 +- 6 files changed, 3 insertions(+), 10 deletions(-) diff --git a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainNavigator.kt b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainNavigator.kt index 0e654c636..e3d695024 100644 --- a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainNavigator.kt +++ b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainNavigator.kt @@ -11,9 +11,9 @@ import androidx.navigation.navOptions import com.suwiki.feature.lectureevaluation.viewerreporter.navigation.navigateLectureEvaluation import com.suwiki.feature.login.navigation.navigateLogin import com.suwiki.feature.myinfo.navigation.navigateMyInfo -import com.suwiki.feature.openmajor.navigation.navigateOpenMajor import com.suwiki.feature.notice.navigation.navigateNotice import com.suwiki.feature.notice.navigation.navigateNoticeDetail +import com.suwiki.feature.openmajor.navigation.navigateOpenMajor import com.suwiki.feature.timetable.navigation.TimetableRoute import com.suwiki.feature.timetable.navigation.navigateTimetable diff --git a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt index 260f492a3..31d79f3ab 100644 --- a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt +++ b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt @@ -41,10 +41,10 @@ import com.suwiki.feature.login.R import com.suwiki.feature.login.navigation.loginNavGraph import com.suwiki.feature.login.navigation.navigateLogin import com.suwiki.feature.myinfo.navigation.myInfoNavGraph +import com.suwiki.feature.notice.navigation.noticeNavGraph import com.suwiki.feature.openmajor.OpenMajorRoute import com.suwiki.feature.openmajor.navigation.OpenMajorRoute import com.suwiki.feature.openmajor.navigation.openMajorNavGraph -import com.suwiki.feature.notice.navigation.noticeNavGraph import com.suwiki.feature.timetable.navigation.timetableNavGraph import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList diff --git a/feature/notice/src/main/java/com/suwiki/feature/notice/NoticeViewModel.kt b/feature/notice/src/main/java/com/suwiki/feature/notice/NoticeViewModel.kt index 3c72b368f..f25617e8f 100644 --- a/feature/notice/src/main/java/com/suwiki/feature/notice/NoticeViewModel.kt +++ b/feature/notice/src/main/java/com/suwiki/feature/notice/NoticeViewModel.kt @@ -1,13 +1,9 @@ package com.suwiki.feature.notice import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.suwiki.core.model.notice.Notice import com.suwiki.domain.notice.usecase.GetNoticeListUseCase import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.launch import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.syntax.simple.intent diff --git a/feature/notice/src/main/java/com/suwiki/feature/notice/detail/NoticeDetailContract.kt b/feature/notice/src/main/java/com/suwiki/feature/notice/detail/NoticeDetailContract.kt index 6753e2dae..6b4c57a71 100644 --- a/feature/notice/src/main/java/com/suwiki/feature/notice/detail/NoticeDetailContract.kt +++ b/feature/notice/src/main/java/com/suwiki/feature/notice/detail/NoticeDetailContract.kt @@ -1,7 +1,6 @@ package com.suwiki.feature.notice.detail import com.suwiki.core.model.notice.NoticeDetail -import com.suwiki.feature.notice.NoticeSideEffect data class NoticeDetailState( val isLoading: Boolean = false, diff --git a/feature/notice/src/main/java/com/suwiki/feature/notice/detail/NoticeDetailViewModel.kt b/feature/notice/src/main/java/com/suwiki/feature/notice/detail/NoticeDetailViewModel.kt index d501719c4..d8db69d3f 100644 --- a/feature/notice/src/main/java/com/suwiki/feature/notice/detail/NoticeDetailViewModel.kt +++ b/feature/notice/src/main/java/com/suwiki/feature/notice/detail/NoticeDetailViewModel.kt @@ -2,8 +2,6 @@ package com.suwiki.feature.notice.detail import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel -import com.suwiki.core.model.exception.UnknownException -import com.suwiki.core.model.notice.NoticeDetail import com.suwiki.domain.notice.usecase.GetNoticeDetailUseCase import com.suwiki.feature.notice.navigation.NoticeRoute import dagger.hilt.android.lifecycle.HiltViewModel diff --git a/feature/notice/src/main/java/com/suwiki/feature/notice/navigation/NoticeNavigation.kt b/feature/notice/src/main/java/com/suwiki/feature/notice/navigation/NoticeNavigation.kt index 0987380d1..1d5b354e2 100644 --- a/feature/notice/src/main/java/com/suwiki/feature/notice/navigation/NoticeNavigation.kt +++ b/feature/notice/src/main/java/com/suwiki/feature/notice/navigation/NoticeNavigation.kt @@ -6,8 +6,8 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavType import androidx.navigation.compose.composable import androidx.navigation.navArgument -import com.suwiki.feature.notice.detail.NoticeDetailRoute import com.suwiki.feature.notice.NoticeRoute +import com.suwiki.feature.notice.detail.NoticeDetailRoute fun NavController.navigateNotice() { navigate(NoticeRoute.route)