diff --git a/data/src/main/java/in/koreatech/koin/data/api/ArticleApi.kt b/data/src/main/java/in/koreatech/koin/data/api/ArticleApi.kt index c23a3b024..4ef52a972 100644 --- a/data/src/main/java/in/koreatech/koin/data/api/ArticleApi.kt +++ b/data/src/main/java/in/koreatech/koin/data/api/ArticleApi.kt @@ -1,7 +1,6 @@ package `in`.koreatech.koin.data.api import `in`.koreatech.koin.data.response.article.ArticleLostAndFoundPaginationResponse -import `in`.koreatech.koin.data.response.article.ArticleLostAndFoundResponse import `in`.koreatech.koin.data.response.article.ArticleLostAndFoundStatsResponse import `in`.koreatech.koin.data.response.article.ArticlePaginationResponse import `in`.koreatech.koin.data.response.article.ArticleResponse @@ -74,15 +73,6 @@ interface ArticleApi { @Query("limit") limit: Int ): ArticleLostAndFoundPaginationResponse - /** - * 분실물 게시글 조회 - * @param id 게시글 아이디 - */ - @GET("articles/lost-item/v2/{id}") - suspend fun fetchArticleLostAndFoundV2( - @Path("id") id: Int - ): ArticleLostAndFoundResponse - /** * 분실물 게시글 통계 조회 */ diff --git a/data/src/main/java/in/koreatech/koin/data/api/auth/ArticleAuthApi.kt b/data/src/main/java/in/koreatech/koin/data/api/auth/ArticleAuthApi.kt index 23a5484ff..bcf5ade3f 100644 --- a/data/src/main/java/in/koreatech/koin/data/api/auth/ArticleAuthApi.kt +++ b/data/src/main/java/in/koreatech/koin/data/api/auth/ArticleAuthApi.kt @@ -77,10 +77,19 @@ interface ArticleAuthApi { @Query("type") type: String?, @Query("page") page: Int, @Query("limit") limit: Int, - @Query("category") category: String?, + @Query("category") category: List, @Query("foundStatus") foundStatus: String?, @Query("sort") sort: String?, @Query("author") author: String?, @Query("title") title: String? ): ArticleLostAndFoundPaginationResponse + + /** + * 분실물 게시글 조회 + * @param id 게시글 아이디 + */ + @GET("articles/lost-item/v2/{id}") + suspend fun fetchArticleLostAndFoundV2( + @Path("id") id: Int + ): ArticleLostAndFoundResponse } diff --git a/data/src/main/java/in/koreatech/koin/data/mapper/ArticleMapper.kt b/data/src/main/java/in/koreatech/koin/data/mapper/ArticleMapper.kt index 237f4283f..967c6d418 100644 --- a/data/src/main/java/in/koreatech/koin/data/mapper/ArticleMapper.kt +++ b/data/src/main/java/in/koreatech/koin/data/mapper/ArticleMapper.kt @@ -12,7 +12,7 @@ fun List.toArticleLostAndFoundRequest(): ArticleLostA fun ArticleLostAndFoundUpload.toArticleLostAndFoundBody(): ArticleLostAndFoundRequest.ArticleLostAndFoundBody { return ArticleLostAndFoundRequest.ArticleLostAndFoundBody( category = category, - foundPlace = foundPlace, + foundPlace = foundPlace.ifEmpty { "장소 미상" }, foundDate = foundDate, content = content, images = images, diff --git a/data/src/main/java/in/koreatech/koin/data/response/article/ArticleLostAndFoundResponse.kt b/data/src/main/java/in/koreatech/koin/data/response/article/ArticleLostAndFoundResponse.kt index 6fa511b2c..9ba36b9b4 100644 --- a/data/src/main/java/in/koreatech/koin/data/response/article/ArticleLostAndFoundResponse.kt +++ b/data/src/main/java/in/koreatech/koin/data/response/article/ArticleLostAndFoundResponse.kt @@ -77,7 +77,7 @@ data class ArticleLostAndFoundResponse( content = content, author = author, organization = organization?.toArticleLostAndFoundOrganization(), - isMine = isMine!!, // Should not be null + isMine = isMine ?: false, // Should not be null isFound = isFound, images = images?.map { it.toArticleLostAndFoundImage() }, registeredAt = registeredAt, diff --git a/data/src/main/java/in/koreatech/koin/data/source/remote/ArticleRemoteDataSource.kt b/data/src/main/java/in/koreatech/koin/data/source/remote/ArticleRemoteDataSource.kt index ca703ec06..03a3e8190 100644 --- a/data/src/main/java/in/koreatech/koin/data/source/remote/ArticleRemoteDataSource.kt +++ b/data/src/main/java/in/koreatech/koin/data/source/remote/ArticleRemoteDataSource.kt @@ -85,7 +85,7 @@ class ArticleRemoteDataSource @Inject constructor( type: String?, page: Int, limit: Int, - category: String?, + category: List, foundStatus: String?, sort: String?, author: String?, @@ -112,7 +112,7 @@ class ArticleRemoteDataSource @Inject constructor( } suspend fun fetchArticleLostAndFoundV2(articleId: Int): ArticleLostAndFoundResponse { - return articleApi.fetchArticleLostAndFoundV2(articleId) + return articleAuthApi.fetchArticleLostAndFoundV2(articleId) } suspend fun uploadArticleLostAndFound(articleLostAndFound: List): Result { diff --git a/domain/src/main/java/in/koreatech/koin/domain/model/article/LostAndFoundFilterParams.kt b/domain/src/main/java/in/koreatech/koin/domain/model/article/LostAndFoundFilterParams.kt index b5dc2ec18..bff8b3309 100644 --- a/domain/src/main/java/in/koreatech/koin/domain/model/article/LostAndFoundFilterParams.kt +++ b/domain/src/main/java/in/koreatech/koin/domain/model/article/LostAndFoundFilterParams.kt @@ -4,7 +4,7 @@ data class LostAndFoundFilterParams( val type: String? = null, val page: Int = 1, val limit: Int = 10, - val category: String = "ALL", + val category: List = emptyList(), val foundStatus: String = "ALL", val sort: String = "LATEST", val author: String = "ALL", diff --git a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/component/EditArticleItemDetail.kt b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/component/EditArticleItemDetail.kt index d317c04f6..6cc34a5ef 100644 --- a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/component/EditArticleItemDetail.kt +++ b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/component/EditArticleItemDetail.kt @@ -3,6 +3,7 @@ package `in`.koreatech.koin.feature.lostandfound.component import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -13,7 +14,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -322,6 +325,46 @@ fun EditArticleItemDetail( ) } } + HorizontalDivider(color = KoinTheme.colors.neutral300) + Box( + modifier = Modifier + .fillMaxWidth() + .background(color = KoinTheme.colors.neutral100) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 2.dp, horizontal = 16.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(id = R.string.date_reset), + style = KoinTheme.typography.medium14, + color = KoinTheme.colors.primary600, + modifier = Modifier + .clickable { + yearPickerState.selectedItemIndex = yearList.indexOf(now.year.toString()) + monthPickerState.selectedItemIndex = monthList.indexOf(now.month.toString()) + dayPickerState.selectedItemIndex = dayList.indexOf(now.dayOfMonth.toString()) + } + .padding(all = 4.dp) + ) + Spacer( + modifier = Modifier.width(24.dp) + ) + Text( + text = stringResource(id = R.string.date_confirm), + style = KoinTheme.typography.medium14, + color = KoinTheme.colors.primary600, + modifier = Modifier + .clickable { + isPickerExpanded = false + } + .padding(all = 4.dp) + ) + } + } } } } @@ -336,14 +379,14 @@ fun EditArticleItemDetail( Text( style = KoinTheme.typography.medium14, text = buildAnnotatedString { - append( - when (type) { - LostOrFoundType.LOST -> stringResource(id = R.string.lost_location) - LostOrFoundType.FOUND -> stringResource(id = R.string.found_location) + when (type) { + LostOrFoundType.LOST -> append(stringResource(id = R.string.lost_location)) + LostOrFoundType.FOUND -> { + append(stringResource(id = R.string.found_location)) + withStyle(style = SpanStyle(color = Color(0xFFC82A2A))) { + append("*") + } } - ) - withStyle(style = SpanStyle(color = Color(0xFFC82A2A))) { - append("*") } } ) diff --git a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/component/SlideUpText.kt b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/component/SlideUpText.kt index ea2ff51ff..86d12d7fb 100644 --- a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/component/SlideUpText.kt +++ b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/component/SlideUpText.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.TextStyle import kotlinx.collections.immutable.ImmutableList @@ -44,6 +45,7 @@ fun SlideUpText( AnimatedContent( targetState = text.value, + contentAlignment = Alignment.Center, transitionSpec = { slideInVertically { height -> height } + fadeIn() togetherWith slideOutVertically { height -> -height } + fadeOut() @@ -52,6 +54,7 @@ fun SlideUpText( Text( modifier = modifier, text = showedText, + maxLines = 1, style = style ) } diff --git a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/enums/LostItemCategory.kt b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/enums/LostItemCategory.kt index 97999a9be..49f9f3e85 100644 --- a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/enums/LostItemCategory.kt +++ b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/enums/LostItemCategory.kt @@ -23,7 +23,7 @@ enum class LostItemCategory( "지갑" -> WALLET "전자제품" -> ELECTRONIC_DEVICE "기타" -> OTHER - else -> NONE + else -> OTHER } } @@ -34,7 +34,7 @@ enum class LostItemCategory( WALLET -> "지갑" ELECTRONIC_DEVICE -> "전자제품" OTHER -> "기타" - NONE -> "" + NONE -> "기타" } } } diff --git a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/navigation/LostAndFoundNavType.kt b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/navigation/LostAndFoundNavType.kt index f4e256690..95d6d59ba 100644 --- a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/navigation/LostAndFoundNavType.kt +++ b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/navigation/LostAndFoundNavType.kt @@ -21,5 +21,5 @@ sealed class LostAndFoundNavType { const val ARTICLE_ID = "articleId" const val CHAT_ARTICLE_ID = "article_id" - const val LOST_OR_FOUND_TYPE = "lostOrFoundType" +const val CANCEL_REFRESH_LIST = "cancel_refresh_list" diff --git a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/navigation/Navigation.kt b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/navigation/Navigation.kt index eb55f36b2..e17e1646d 100644 --- a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/navigation/Navigation.kt +++ b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/navigation/Navigation.kt @@ -1,7 +1,11 @@ package `in`.koreatech.koin.feature.lostandfound.navigation +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext -import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable @@ -18,12 +22,44 @@ fun NavGraphBuilder.koinLostAndFoundGraph( navController: NavController, onBackPressed: () -> Unit ) { + val cancelRefreshList = { cancelRefresh: Boolean -> + navController.getBackStackEntry(LostAndFoundNavType.LostAndFoundListRoute) + ?.savedStateHandle + ?.let { handle -> + if (handle.get(CANCEL_REFRESH_LIST) != false) { + handle[CANCEL_REFRESH_LIST] = cancelRefresh + } + } + } + + val navigateToList = { cancelRefresh: Boolean -> + cancelRefreshList(cancelRefresh) + navController.navigate(LostAndFoundNavType.LostAndFoundListRoute) { + popUpTo(navController.graph.startDestinationId) { + inclusive = false + } + launchSingleTop = true + } + } + + val onBackPressed = { + cancelRefreshList(true) + onBackPressed() + } + composable { backStackEntry -> - val refreshFlow = backStackEntry.savedStateHandle.getStateFlow("refresh_list", false).collectAsStateWithLifecycle() + var isCancelRefresh by remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + if (backStackEntry.savedStateHandle.contains(CANCEL_REFRESH_LIST)) { + isCancelRefresh = backStackEntry.savedStateHandle[CANCEL_REFRESH_LIST] ?: false + } + backStackEntry.savedStateHandle.remove(CANCEL_REFRESH_LIST) + } val navigator = rememberNavigator() val context = LocalContext.current LostAndFoundList( - doRefresh = refreshFlow.value, + cancelRefresh = isCancelRefresh, onTopbarBackClick = onBackPressed, navigateArticleDetail = { articleId -> navController.navigate(LostAndFoundNavType.LostAndFoundDetailRoute(articleId)) @@ -46,20 +82,9 @@ fun NavGraphBuilder.koinLostAndFoundGraph( val navigator = rememberNavigator() val context = LocalContext.current LostAndFoundDetail( - refreshLostAndFoundList = { - navController.getBackStackEntry(LostAndFoundNavType.LostAndFoundListRoute) - ?.savedStateHandle - ?.set("refresh_list", true) - }, - navigateToArticleList = { - navController.navigate(LostAndFoundNavType.LostAndFoundListRoute) { - popUpTo(navController.graph.startDestinationId) { - inclusive = false - } - launchSingleTop = true - } - }, + navigateToArticleList = navigateToList, onTopbarBackClick = onBackPressed, + refreshList = { cancelRefreshList(false) }, navigateToChatRoom = { articleId -> val intent = navigator.navigateToChatRoom(context) intent.putExtra(CHAT_ARTICLE_ID, articleId) @@ -89,21 +114,15 @@ fun NavGraphBuilder.koinLostAndFoundGraph( val route = backStackEntry.toRoute() LostAndFoundReport( articleId = route.articleId, - onSuccess = { navController.navigateUp() } + onTopbarBackClick = onBackPressed, + onSuccess = { navigateToList(false) } ) } composable { LostAndFoundWriteArticle( onBackClick = onBackPressed, - onComplete = { - navController.navigate(LostAndFoundNavType.LostAndFoundListRoute) { - popUpTo(navController.graph.startDestinationId) { - inclusive = true - } - launchSingleTop = true - } - } + onComplete = { navigateToList(false) } ) } @@ -111,11 +130,7 @@ fun NavGraphBuilder.koinLostAndFoundGraph( LostAndFoundModify( onBackClick = onBackPressed, onComplete = { articleId -> - navController.navigate(LostAndFoundNavType.LostAndFoundListRoute) { - popUpTo { - inclusive = true - } - } + navigateToList(false) navController.navigate(LostAndFoundNavType.LostAndFoundDetailRoute(articleId)) } ) diff --git a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/detail/LostAndFoundDetail.kt b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/detail/LostAndFoundDetail.kt index b1fcc9c08..f46b90f66 100644 --- a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/detail/LostAndFoundDetail.kt +++ b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/detail/LostAndFoundDetail.kt @@ -50,9 +50,9 @@ import org.orbitmvi.orbit.compose.collectSideEffect fun LostAndFoundDetail( viewModel: LostAndFoundDetailViewModel = hiltViewModel(), modifier: Modifier = Modifier, - navigateToArticleList: () -> Unit = {}, onTopbarBackClick: () -> Unit = {}, - refreshLostAndFoundList: () -> Unit = {}, + refreshList: () -> Unit = {}, + navigateToArticleList: (cancelRefresh: Boolean) -> Unit = {}, navigateToRecentArticle: (articleId: Int) -> Unit = {}, navigateToChatRoom: (articleId: Int) -> Unit = {}, navigateToLogin: (articleId: Int) -> Unit = {}, @@ -64,7 +64,9 @@ fun LostAndFoundDetail( topBar = { KoinTopAppBar( title = stringResource(R.string.lost_and_found), - onNavigationIconClick = onTopbarBackClick + onNavigationIconClick = { + onTopbarBackClick() + } ) } ) { contentPadding -> @@ -84,8 +86,8 @@ fun LostAndFoundDetail( loggingLostOrFound ) viewModel.setFound() + refreshList() viewModel.setShowFoundDialog(false) - refreshLostAndFoundList() }, onNegative = { viewModel.setShowFoundDialog(false) @@ -121,7 +123,7 @@ fun LostAndFoundDetail( } viewModel.collectSideEffect { - handleSideEffect(it, context, navigateToArticleList, refreshLostAndFoundList) + handleSideEffect(it, context, navigateToArticleList) } Column( @@ -137,6 +139,14 @@ fun LostAndFoundDetail( val enableRecentArticleHeight = remember(layoutHeightDp.value) { mutableStateOf(screenHeightDp - (contentPadding.calculateTopPadding() + contentPadding.calculateBottomPadding()) - layoutHeightDp.value) } + + val itemHeightDp = 48.dp + val headerHeight = 54.dp + val finalRecentArticleHeight = remember(enableRecentArticleHeight.value) { + val availableHeight = enableRecentArticleHeight.value - headerHeight + val visibleItemCount = (availableHeight / itemHeightDp).toInt() + headerHeight + (itemHeightDp * (visibleItemCount + 0.65f)) + } Layout( content = { Column { @@ -194,7 +204,7 @@ fun LostAndFoundDetail( isAuthorWithdraw = uiState.isAuthorWithdraw, isWriterAdmin = uiState.organization != null, onArticleListClick = { - navigateToArticleList + navigateToArticleList(true) }, onDeleteArticleClick = { viewModel.deleteArticle() @@ -240,8 +250,8 @@ fun LostAndFoundDetail( RecentArticleList( modifier = Modifier - .heightIn(min = 300.dp, max = screenHeightDp) - .height(enableRecentArticleHeight.value), + .heightIn(min = 275.dp, max = screenHeightDp) + .height(finalRecentArticleHeight), recentArticles = recentArticles, isLoadingMore = uiState.isLoadingMoreArticles, hasMoreArticles = uiState.hasMoreArticles, @@ -263,8 +273,7 @@ fun LostAndFoundDetail( private fun handleSideEffect( sideEffect: LostAndFoundDetailSideEffect, context: Context, - navigateToArticleList: () -> Unit = {}, - refreshLostAndFoundList: () -> Unit = {} + navigateToArticleList: (cancelRefresh: Boolean) -> Unit = {} ) { when (sideEffect) { is LostAndFoundDetailSideEffect.DeleteArticle -> { @@ -273,8 +282,7 @@ private fun handleSideEffect( context.getString(R.string.detail_delete_toast), Toast.LENGTH_SHORT ).show() - refreshLostAndFoundList() - navigateToArticleList() + navigateToArticleList(false) } LostAndFoundDetailSideEffect.DeleteArticleFailed -> { @@ -291,8 +299,7 @@ private fun handleSideEffect( context.getString(R.string.detail_deleted_article), Toast.LENGTH_SHORT ).show() - refreshLostAndFoundList() - navigateToArticleList() + navigateToArticleList(true) } LostAndFoundDetailSideEffect.UpdateFoundFail -> { diff --git a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/detail/LostAndFoundDetailState.kt b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/detail/LostAndFoundDetailState.kt index 071d77864..495c7464d 100644 --- a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/detail/LostAndFoundDetailState.kt +++ b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/detail/LostAndFoundDetailState.kt @@ -13,7 +13,6 @@ import kotlinx.parcelize.Parcelize data class LostAndFoundDetailState( val isLoading: Boolean = false, val isLoggedIn: Boolean = false, - val currentLoggedInUser: String = "", val showDeleteDialog: Boolean = false, val showFoundDialog: Boolean = false, val showLoginDialog: Boolean = false, diff --git a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/detail/LostAndFoundDetailViewModel.kt b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/detail/LostAndFoundDetailViewModel.kt index 67a67d5d8..d40249daa 100644 --- a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/detail/LostAndFoundDetailViewModel.kt +++ b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/detail/LostAndFoundDetailViewModel.kt @@ -54,16 +54,12 @@ class LostAndFoundDetailViewModel @Inject constructor( when (it) { is User.Student -> reduce { state.copy( - isLoggedIn = true, - currentLoggedInUser = it.nickname ?: "", - isMine = it.nickname == state.author + isLoggedIn = true ) } is User.General -> reduce { state.copy( - isLoggedIn = true, - currentLoggedInUser = it.nickname ?: "", - isMine = it.nickname == state.author + isLoggedIn = true ) } is User.Anonymous -> reduce { @@ -100,7 +96,7 @@ class LostAndFoundDetailViewModel @Inject constructor( registeredAt = article.registeredAt, updatedAt = article.updatedAt, organization = article.organization, - isMine = state.currentLoggedInUser == article.author, + isMine = article.isMine, isAuthorWithdraw = article.author == "탈퇴한 사용자", isLoading = false, isFound = article.isFound diff --git a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/list/LostAndFoundList.kt b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/list/LostAndFoundList.kt index c7699bbe6..766c630c0 100644 --- a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/list/LostAndFoundList.kt +++ b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/list/LostAndFoundList.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBarsPadding @@ -16,8 +17,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -38,11 +38,13 @@ import `in`.koreatech.koin.feature.lostandfound.ui.list.component.LostAndFoundFA import `in`.koreatech.koin.feature.lostandfound.ui.list.component.LostAndFoundFABBottomSheet import `in`.koreatech.koin.feature.lostandfound.ui.list.component.LostAndFoundFilterBottomSheet import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged import org.orbitmvi.orbit.compose.collectAsState @Composable fun LostAndFoundList( - doRefresh: Boolean, + cancelRefresh: Boolean, viewModel: LostAndFoundListViewModel = hiltViewModel(), onTopbarBackClick: () -> Unit = {}, navigateToLogin: () -> Unit = {}, @@ -51,13 +53,18 @@ fun LostAndFoundList( ) { val uiState by viewModel.collectAsState() - val refresh = remember(doRefresh) { mutableStateOf(doRefresh) } - - LaunchedEffect(refresh) { - if (doRefresh) { - viewModel.fetchLostAndFoundItem() - refresh.value = false - } + LaunchedEffect(Unit, cancelRefresh) { + var cancelRefresh = cancelRefresh + snapshotFlow { uiState.searchQuery } + .debounce(SEARCH_DEBOUNCE_MS) + .distinctUntilChanged() + .collect { + if (cancelRefresh) { + cancelRefresh = false + } else { + viewModel.fetchLostAndFoundItem() + } + } } if (uiState.showFilterBottomSheet) { @@ -145,6 +152,7 @@ fun LostAndFoundList( }, floatingActionButton = { LostAndFoundFAB( + modifier = Modifier.offset(y = 10.dp), onClick = { if (uiState.isLoggedIn) { viewModel.setShowWriteBottomSheet(true) @@ -165,7 +173,10 @@ fun LostAndFoundList( Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 24.dp), + .padding( + horizontal = 24.dp, + vertical = 4.dp + ), verticalAlignment = Alignment.CenterVertically ) { ItemSearchTextField( @@ -220,3 +231,5 @@ fun LostAndFoundList( } } } + +const val SEARCH_DEBOUNCE_MS = 250L diff --git a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/list/LostAndFoundListState.kt b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/list/LostAndFoundListState.kt index 05e06e877..402580a52 100644 --- a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/list/LostAndFoundListState.kt +++ b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/list/LostAndFoundListState.kt @@ -16,11 +16,11 @@ data class LostAndFoundListState( val isLoggedIn: Boolean = false, val showFilterLoginDialog: Boolean = false, val showWriteLoginDialog: Boolean = false, - val isFirstPageLoading: Boolean = false, + val isFirstPageLoading: Boolean = true, val showFilterBottomSheet: Boolean = false, val showWriteBottomSheet: Boolean = false, val searchQuery: String = "", - val categoryFilterType: CategoryFilterType = CategoryFilterType.ALL, + val categoryFilterType: ImmutableList = persistentListOf(CategoryFilterType.ALL), val lostOrFoundFilterType: LostOrFoundFilterType = LostOrFoundFilterType.ALL, val foundFilterType: FoundFilterType = FoundFilterType.ALL, val authorFilterType: AuthorFilterType = AuthorFilterType.ALL, diff --git a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/list/LostAndFoundListViewModel.kt b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/list/LostAndFoundListViewModel.kt index 4ad726002..dff7e4485 100644 --- a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/list/LostAndFoundListViewModel.kt +++ b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/list/LostAndFoundListViewModel.kt @@ -20,11 +20,6 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.syntax.simple.intent @@ -41,14 +36,8 @@ class LostAndFoundListViewModel @Inject constructor( initialState = LostAndFoundListState() ) - companion object { - const val SEARCH_DEBOUNCE_MS = 300L - } - init { initUserInfo() - fetchLostAndFoundItem() - observeQuery() } private fun initUserInfo() = viewModelScope.launch { @@ -90,7 +79,7 @@ class LostAndFoundListViewModel @Inject constructor( val filterParams = LostAndFoundFilterParams( page = 1, limit = PAGE_SIZE, - category = state.categoryFilterType.value, + category = state.categoryFilterType.map { it.value }, foundStatus = state.foundFilterType.value, author = state.authorFilterType.value, type = type, @@ -141,7 +130,7 @@ class LostAndFoundListViewModel @Inject constructor( val filterParams = LostAndFoundFilterParams( page = nextPage, limit = PAGE_SIZE, - category = state.categoryFilterType.value, + category = state.categoryFilterType.map { it.value }, foundStatus = state.foundFilterType.value, author = state.authorFilterType.value, type = type, @@ -159,7 +148,7 @@ class LostAndFoundListViewModel @Inject constructor( val filteredArticles = pagination.articleLostAndFoundHeader.map { it.toLostAndFoundItemState() } reduce { state.copy( - searchedArticles = (state.searchedArticles + filteredArticles).toPersistentList(), + searchedArticles = (state.searchedArticles + filteredArticles).distinctBy { it.id }.toPersistentList(), searchedArticlesCurrentPage = pagination.currentPage, searchedArticlesTotalPage = pagination.totalPage, hasMoreArticles = pagination.currentPage < pagination.totalPage, @@ -172,14 +161,14 @@ class LostAndFoundListViewModel @Inject constructor( fun setSearchFilter( authorFilterType: AuthorFilterType, lostOrFoundFilterType: LostOrFoundFilterType, - categoryFilterType: CategoryFilterType, + categoryFilterType: List, foundFilterType: FoundFilterType ) = intent { reduce { state.copy( authorFilterType = authorFilterType, lostOrFoundFilterType = lostOrFoundFilterType, - categoryFilterType = categoryFilterType, + categoryFilterType = categoryFilterType.toPersistentList(), foundFilterType = foundFilterType ) } @@ -224,15 +213,4 @@ class LostAndFoundListViewModel @Inject constructor( ) } } - - private fun observeQuery() { - container.stateFlow - .map { it.searchQuery } - .debounce(SEARCH_DEBOUNCE_MS) - .distinctUntilChanged() - .onEach { query -> - fetchLostAndFoundItem() - } - .launchIn(viewModelScope) - } } diff --git a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/list/component/FABBottomsheet.kt b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/list/component/FABBottomsheet.kt index a42878be9..717e63235 100644 --- a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/list/component/FABBottomsheet.kt +++ b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/list/component/FABBottomsheet.kt @@ -20,6 +20,7 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector @@ -28,6 +29,7 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import `in`.koreatech.koin.core.designsystem.theme.KoinTheme import `in`.koreatech.koin.feature.lostandfound.R +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -39,6 +41,7 @@ fun LostAndFoundFABBottomSheet( val sheetState = rememberModalBottomSheetState( skipPartiallyExpanded = true ) + val scope = rememberCoroutineScope() ModalBottomSheet( sheetState = sheetState, @@ -47,13 +50,17 @@ fun LostAndFoundFABBottomSheet( dragHandle = null ) { LostAndFoundFABContent( - onDismissRequest = onDismissRequest, + onDismissRequest = { + scope.launch { sheetState.hide() } + onDismissRequest() + }, onFindOwnerClick = onFindOwnerClick, onLostItemClick = onLostItemClick ) } } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun LostAndFoundFABContent( onDismissRequest: () -> Unit, @@ -109,8 +116,8 @@ fun LostAndFoundFABContent( text = stringResource(R.string.finding_btn), icon = ImageVector.vectorResource(R.drawable.ic_found), onClick = { - onFindOwnerClick() onDismissRequest() + onFindOwnerClick() } ) Spacer(modifier = Modifier.height(16.dp)) @@ -118,8 +125,8 @@ fun LostAndFoundFABContent( text = stringResource(R.string.lost_btn), icon = ImageVector.vectorResource(R.drawable.ic_lost), onClick = { - onLostItemClick() onDismissRequest() + onLostItemClick() } ) } diff --git a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/list/component/FilterBottomsheet.kt b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/list/component/FilterBottomsheet.kt index aa6482a2d..7d146686c 100644 --- a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/list/component/FilterBottomsheet.kt +++ b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/list/component/FilterBottomsheet.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -25,6 +26,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -44,6 +46,8 @@ import `in`.koreatech.koin.feature.lostandfound.enums.LostAndFoundFilterType.Fou import `in`.koreatech.koin.feature.lostandfound.enums.LostAndFoundFilterType.LostOrFoundFilterType import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -51,9 +55,9 @@ fun LostAndFoundFilterBottomSheet( onDismissRequest: () -> Unit, selectedAuthorType: AuthorFilterType, selectedLostOrFoundType: LostOrFoundFilterType, - selectedCategoryType: CategoryFilterType, + selectedCategoryType: ImmutableList, selectedFoundType: FoundFilterType, - onApply: (AuthorFilterType, LostOrFoundFilterType, CategoryFilterType, FoundFilterType) -> Unit + onApply: (AuthorFilterType, LostOrFoundFilterType, ImmutableList, FoundFilterType) -> Unit ) { var selectedAuthorType by remember { mutableStateOf(selectedAuthorType) } var selectedLostOrFoundType by remember { mutableStateOf(selectedLostOrFoundType) } @@ -63,6 +67,7 @@ fun LostAndFoundFilterBottomSheet( val sheetState = rememberModalBottomSheetState( skipPartiallyExpanded = true ) + val scope = rememberCoroutineScope() ModalBottomSheet( sheetState = sheetState, @@ -78,13 +83,25 @@ fun LostAndFoundFilterBottomSheet( onAuthorTypeChange = { selectedAuthorType = it as AuthorFilterType }, onLostOrFoundTypeChange = { selectedLostOrFoundType = it as LostOrFoundFilterType }, - onCategoryTypeChange = { selectedCategoryType = it as CategoryFilterType }, + onCategoryTypeChange = { + val newSelectedCategories = it.map { type -> type as CategoryFilterType } + selectedCategoryType = if ( + selectedCategoryType.size == 1 && + selectedCategoryType.first() == CategoryFilterType.ALL + ) { + (newSelectedCategories - CategoryFilterType.ALL).toPersistentList() + } else if (CategoryFilterType.ALL in newSelectedCategories) { + persistentListOf(CategoryFilterType.ALL) + } else { + newSelectedCategories.toPersistentList() + } + }, onFoundTypeChange = { selectedFoundType = it as FoundFilterType }, onReset = { selectedAuthorType = AuthorFilterType.ALL selectedLostOrFoundType = LostOrFoundFilterType.ALL - selectedCategoryType = CategoryFilterType.ALL + selectedCategoryType = persistentListOf(CategoryFilterType.ALL) selectedFoundType = FoundFilterType.ALL }, @@ -97,7 +114,10 @@ fun LostAndFoundFilterBottomSheet( ) }, - onDismissRequest = onDismissRequest + onDismissRequest = { + scope.launch { sheetState.hide() } + onDismissRequest() + } ) } } @@ -106,11 +126,11 @@ fun LostAndFoundFilterBottomSheet( fun FilterBottomSheetContent( selectedAuthorType: AuthorFilterType, selectedLostOrFoundType: LostOrFoundFilterType, - selectedCategoryType: CategoryFilterType, + selectedCategoryType: ImmutableList, selectedFoundType: FoundFilterType, onAuthorTypeChange: (LostAndFoundFilterType) -> Unit, onLostOrFoundTypeChange: (LostAndFoundFilterType) -> Unit, - onCategoryTypeChange: (LostAndFoundFilterType) -> Unit, + onCategoryTypeChange: (ImmutableList) -> Unit, onFoundTypeChange: (LostAndFoundFilterType) -> Unit, onReset: () -> Unit, onApplyClick: () -> Unit, @@ -170,7 +190,7 @@ fun FilterBottomSheetContent( onItemSelected = onLostOrFoundTypeChange ) HorizontalDivider(color = KoinTheme.colors.neutral300) - FilterSection( + FilterDuplicateSection( title = stringResource(R.string.filter_list_type), items = persistentListOf( CategoryFilterType.ALL, @@ -180,7 +200,7 @@ fun FilterBottomSheetContent( CategoryFilterType.ELECTRONIC, CategoryFilterType.OTHER ), - selectedItem = selectedCategoryType, + selectedItems = selectedCategoryType, onItemSelected = onCategoryTypeChange ) HorizontalDivider(color = KoinTheme.colors.neutral300) @@ -282,3 +302,47 @@ fun FilterSection( } } } +private const val AT_LEAST_COUNT = 1 + +@Composable +fun FilterDuplicateSection( + title: String, + items: ImmutableList, + selectedItems: ImmutableList, + onItemSelected: (ImmutableList) -> Unit +) { + Column(modifier = Modifier.padding(vertical = 12.dp)) { + Text( + text = title, + style = KoinTheme.typography.bold16, + color = KoinTheme.colors.neutral800, + modifier = Modifier.padding(bottom = 12.dp) + ) + FlowRow( + modifier = Modifier.fillMaxWidth(), + maxItemsInEachRow = 3, + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + items.forEach { item -> + FilterChipCustom( + text = stringResource(item.stringRes), + isSelected = item in selectedItems, + onClick = { + onItemSelected( + if (item in selectedItems) { + if (selectedItems.size > AT_LEAST_COUNT) { + (selectedItems - item).toPersistentList() + } else { + return@FilterChipCustom + } + } else { + (selectedItems + item).toPersistentList() + } + ) + } + ) + } + } + } +} diff --git a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/list/component/ListItem.kt b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/list/component/ListItem.kt index d55674e73..1d30b7b71 100644 --- a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/list/component/ListItem.kt +++ b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/list/component/ListItem.kt @@ -66,7 +66,7 @@ fun ListItem( Column( modifier = modifier .fillMaxWidth() - .clickable { + .clickable(enabled = !isReported) { EventLogger.logCampusClickEvent( AnalyticsConstant.Label.LostAndFound.LOST_ITEM_POST_ENTRY, loggingEntry diff --git a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/modify/LostAndFoundModifyViewModel.kt b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/modify/LostAndFoundModifyViewModel.kt index c8b7dc244..363c48e47 100644 --- a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/modify/LostAndFoundModifyViewModel.kt +++ b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/modify/LostAndFoundModifyViewModel.kt @@ -13,6 +13,7 @@ import `in`.koreatech.koin.domain.usecase.presignedurl.UploadImageUseCase import `in`.koreatech.koin.feature.lostandfound.IMAGE_MAX_COUNT import `in`.koreatech.koin.feature.lostandfound.enums.LostItemCategory import `in`.koreatech.koin.feature.lostandfound.enums.LostItemCategory.Companion.getCategoryKoreanWord +import `in`.koreatech.koin.feature.lostandfound.enums.LostOrFoundType import `in`.koreatech.koin.feature.lostandfound.navigation.ARTICLE_ID import java.time.LocalDate import java.time.format.DateTimeFormatter @@ -151,7 +152,7 @@ class LostAndFoundModifyViewModel @Inject constructor( reduce { state.copy( foundPlace = foundPlace, - locationRequired = foundPlace.isEmpty() + locationRequired = foundPlace.isEmpty() && state.lostOrFoundType == LostOrFoundType.FOUND ) } } @@ -170,7 +171,7 @@ class LostAndFoundModifyViewModel @Inject constructor( reduce { state.copy( itemTypeRequired = state.category == LostItemCategory.NONE, - locationRequired = state.foundPlace.isEmpty() + locationRequired = state.foundPlace.isEmpty() && state.lostOrFoundType == LostOrFoundType.FOUND ) } modifyArticle() diff --git a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/report/LostAndFoundReport.kt b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/report/LostAndFoundReport.kt index 87a7e39c2..814663705 100644 --- a/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/report/LostAndFoundReport.kt +++ b/feature/lostandfound/src/main/java/in/koreatech/koin/feature/lostandfound/ui/report/LostAndFoundReport.kt @@ -1,6 +1,5 @@ package `in`.koreatech.koin.feature.lostandfound.ui.report -import android.app.Activity import android.content.Context import android.widget.Toast import androidx.compose.foundation.layout.consumeWindowInsets @@ -29,6 +28,7 @@ import timber.log.Timber @Composable fun LostAndFoundReport( articleId: Int, + onTopbarBackClick: () -> Unit = {}, onSuccess: () -> Unit, viewModel: LostAndFoundReportViewModel = hiltViewModel() ) { @@ -45,7 +45,7 @@ fun LostAndFoundReport( topBar = { KoinTopAppBar( title = stringResource(R.string.report_title), - onNavigationIconClick = { (context as Activity).finish() }, + onNavigationIconClick = onTopbarBackClick, colors = TopAppBarDefaults.centerAlignedTopAppBarColors().copy( containerColor = KoinTheme.colors.primary500, navigationIconContentColor = KoinTheme.colors.neutral0, @@ -89,6 +89,11 @@ fun handleSideEffect( when (sideEffect) { is LostAndFoundReportSideEffect.ReportSuccess -> { onSuccess() + Toast.makeText( + context, + context.getString(R.string.report_success), + Toast.LENGTH_SHORT + ).show() } is LostAndFoundReportSideEffect.ReportFailure -> { diff --git a/feature/lostandfound/src/main/res/values/strings.xml b/feature/lostandfound/src/main/res/values/strings.xml index fe1f3b52a..d1439e56a 100644 --- a/feature/lostandfound/src/main/res/values/strings.xml +++ b/feature/lostandfound/src/main/res/values/strings.xml @@ -73,6 +73,9 @@ 분실 일자 분실 일자를 입력해주세요. 분실일자가 입력되지 않았습니다. + + 초기화 + 확인 습득 장소 습득 장소를 입력해주세요. @@ -134,7 +137,7 @@ 학생생활 학생 게시글을 작성 하려면\n로그인이 필요해요. - 로그인 후 분실물 주인을 찾아주세요! + 로그인 후 글을 작성해주세요! 글쓰기 주인을 찾아요 @@ -158,6 +161,7 @@ 신고하기 + 게시글이 신고되었습니다. 게시글 신고에 실패하였습니다 신고에 의해 숨김 처리 되었습니다. @@ -224,7 +228,7 @@ 찾음으로 변경하지 못했습니다. - 내 개시물 필터는\n로그인이 필요해요. + 내 게시물 필터는\n로그인이 필요해요. 로그인 후 이용하세요! 로그인하기 닫기