diff --git a/data/study/src/main/java/com/umcspot/spot/study/datasource/StudyDataSource.kt b/data/study/src/main/java/com/umcspot/spot/study/datasource/StudyDataSource.kt index f55976b0..74f72d82 100644 --- a/data/study/src/main/java/com/umcspot/spot/study/datasource/StudyDataSource.kt +++ b/data/study/src/main/java/com/umcspot/spot/study/datasource/StudyDataSource.kt @@ -3,6 +3,7 @@ package com.umcspot.spot.study.datasource import com.umcspot.spot.model.FeeRange import com.umcspot.spot.model.RecruitingStatus import com.umcspot.spot.model.RecruitingStudySort +import com.umcspot.spot.model.StudyTheme import com.umcspot.spot.network.model.BaseResponse import com.umcspot.spot.study.dto.request.StudyRequestDto import com.umcspot.spot.study.dto.response.CreateStudyResponseDto @@ -22,6 +23,16 @@ interface StudyDataSource { regionCodes : List? ): BaseResponse + suspend fun getPreferCategoryStudies( + category : StudyTheme?, + recruitingStatus : RecruitingStatus?, + feeCategory: FeeRange?, + isOnline : Boolean?, + sortType: RecruitingStudySort?, + cursor: Long?, + size: Int, + ): BaseResponse + suspend fun createStudy(request: StudyRequestDto, imageFile: File?): BaseResponse suspend fun getCategoryStudies( diff --git a/data/study/src/main/java/com/umcspot/spot/study/datasourceimpl/StudyDataSourceImpl.kt b/data/study/src/main/java/com/umcspot/spot/study/datasourceimpl/StudyDataSourceImpl.kt index 7af51c6c..a6f76bc9 100644 --- a/data/study/src/main/java/com/umcspot/spot/study/datasourceimpl/StudyDataSourceImpl.kt +++ b/data/study/src/main/java/com/umcspot/spot/study/datasourceimpl/StudyDataSourceImpl.kt @@ -3,6 +3,7 @@ package com.umcspot.spot.study.datasourceimpl import com.umcspot.spot.model.FeeRange import com.umcspot.spot.model.RecruitingStatus import com.umcspot.spot.model.RecruitingStudySort +import com.umcspot.spot.model.StudyTheme import com.umcspot.spot.network.model.BaseResponse import com.umcspot.spot.study.datasource.StudyDataSource import com.umcspot.spot.study.dto.request.StudyRequestDto @@ -45,6 +46,17 @@ class StudyDataSourceImpl @Inject constructor( ): BaseResponse = studyService.getPreferLocationStudies(recruitingStatus, feeCategory, categories, null, sortType, cursor, size, regionCodes) + override suspend fun getPreferCategoryStudies( + category: StudyTheme?, + recruitingStatus: RecruitingStatus?, + feeCategory: FeeRange?, + isOnline : Boolean?, + sortType: RecruitingStudySort?, + cursor: Long?, + size: Int, + ): BaseResponse = + studyService.getPreferCategoryStudies(category, recruitingStatus, feeCategory, isOnline, sortType, cursor, size) + override suspend fun createStudy( request: StudyRequestDto, imageFile: File? diff --git a/data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt b/data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt index 332a14b3..caf8fc03 100644 --- a/data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt +++ b/data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt @@ -70,6 +70,30 @@ class StudyRepositoryImpl @Inject constructor( Log.e("StudyRepository", "getPreferLocationStudies failed", it) } + override suspend fun getPreferCategoryStudies( + category: StudyTheme?, + recruitingStatus: RecruitingStatus?, + feeRange: FeeRange?, + isOnline : Boolean?, + sortBy: RecruitingStudySort?, + cursor: Long?, + size: Int, + ): Result = + runCatching { + val response = studyDataSource.getPreferCategoryStudies( + category = category, + recruitingStatus = recruitingStatus, + feeCategory = feeRange, + isOnline = isOnline, + sortType = sortBy, + cursor = cursor, + size = size, + ) + response.result.toDomainList() + }.onFailure { + Log.e("StudyRepository", "getPreferCategoryStudies failed", it) + } + override suspend fun getRecommendedStudies(): Result = runCatching { studyDataSource.getRecommendedStudies().result.toDomainList() diff --git a/data/study/src/main/java/com/umcspot/spot/study/service/StudyService.kt b/data/study/src/main/java/com/umcspot/spot/study/service/StudyService.kt index 0980fecc..71800a87 100644 --- a/data/study/src/main/java/com/umcspot/spot/study/service/StudyService.kt +++ b/data/study/src/main/java/com/umcspot/spot/study/service/StudyService.kt @@ -1,6 +1,5 @@ package com.umcspot.spot.study.service -import com.umcspot.spot.model.ActivityType import com.umcspot.spot.model.FeeRange import com.umcspot.spot.model.RecruitingStatus import com.umcspot.spot.model.RecruitingStudySort @@ -43,6 +42,17 @@ interface StudyService { @Query("regionCodes") regionCodes: List? ): BaseResponse + @GET("/api/studies/by-category") + suspend fun getPreferCategoryStudies( + @Query("category") category: StudyTheme?, + @Query("recruitingStatus") recruitingStatus: RecruitingStatus?, + @Query("feeCategory") feeCategory: FeeRange?, + @Query("isOnline") isOnline: Boolean?, + @Query("sortBy") sortBy: RecruitingStudySort?, + @Query("cursor") cursor: Long?, + @Query("size") size: Int, + ): BaseResponse + @GET("/api/studies/categories") suspend fun getCategoryStudies( @Query("recruitingStatus") recruitingStatus: RecruitingStatus?, diff --git a/data/user/src/main/java/com/umcspot/spot/user/dto/request/UserRequestDto.kt b/data/user/src/main/java/com/umcspot/spot/user/dto/request/UserRequestDto.kt index 52f1530d..b4cd8d58 100644 --- a/data/user/src/main/java/com/umcspot/spot/user/dto/request/UserRequestDto.kt +++ b/data/user/src/main/java/com/umcspot/spot/user/dto/request/UserRequestDto.kt @@ -6,8 +6,8 @@ import kotlinx.serialization.Serializable @Serializable data class UserThemeRequestDto( - @SerialName("userThemes") - val userThemes: List + @SerialName("categories") + val categories: List ) @Serializable diff --git a/data/user/src/main/java/com/umcspot/spot/user/mapper/UserMapper.kt b/data/user/src/main/java/com/umcspot/spot/user/mapper/UserMapper.kt index 9098c621..8bbd1d5f 100644 --- a/data/user/src/main/java/com/umcspot/spot/user/mapper/UserMapper.kt +++ b/data/user/src/main/java/com/umcspot/spot/user/mapper/UserMapper.kt @@ -17,7 +17,7 @@ import com.umcspot.spot.user.model.UserPreferredCategoryResult import com.umcspot.spot.user.model.UserPreferredRegionResult fun List.toRequestDto(): UserThemeRequestDto = - UserThemeRequestDto(userThemes = this) + UserThemeRequestDto(categories = this) fun String.toRequestDto(): UserNameRequestDto = UserNameRequestDto(name = this) @@ -53,6 +53,6 @@ fun MyPageResponseDto.toDomain(): MyPageResult = fun UserPreferredCategoryResponseDto.toDomain(): UserPreferredCategoryResult = UserPreferredCategoryResult( - categories = categories.map { StudyTheme.from(it)?.title }, + categories = categories.map { StudyTheme.from(it) }, totalCount = totalCount ) diff --git a/domain/study/src/main/java/com/umcspot/spot/study/repository/StudyRepository.kt b/domain/study/src/main/java/com/umcspot/spot/study/repository/StudyRepository.kt index 60341c8e..41f2370c 100644 --- a/domain/study/src/main/java/com/umcspot/spot/study/repository/StudyRepository.kt +++ b/domain/study/src/main/java/com/umcspot/spot/study/repository/StudyRepository.kt @@ -29,6 +29,16 @@ interface StudyRepository { regionCodes : List? ): Result + suspend fun getPreferCategoryStudies( + category : StudyTheme?, + recruitingStatus : RecruitingStatus?, + feeRange: FeeRange?, + isOnline : Boolean?, + sortBy: RecruitingStudySort?, + cursor: Long?, + size: Int, + ): Result + suspend fun createStudy(studyCreateModel: StudyCreateModel, imageFile: File?): Result suspend fun getCategoryStudies( diff --git a/domain/user/src/main/java/com/umcspot/spot/user/model/UserPreferredCategoryResult.kt b/domain/user/src/main/java/com/umcspot/spot/user/model/UserPreferredCategoryResult.kt index 4e11b657..06316941 100644 --- a/domain/user/src/main/java/com/umcspot/spot/user/model/UserPreferredCategoryResult.kt +++ b/domain/user/src/main/java/com/umcspot/spot/user/model/UserPreferredCategoryResult.kt @@ -1,6 +1,8 @@ package com.umcspot.spot.user.model +import com.umcspot.spot.model.StudyTheme + data class UserPreferredCategoryResult( - val categories : List, + val categories : List, val totalCount: Int ) \ No newline at end of file diff --git a/feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt b/feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt index 4c9237bf..520e64eb 100644 --- a/feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt +++ b/feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt @@ -34,6 +34,10 @@ import com.umcspot.spot.signup.navigation.signupGraph import com.umcspot.spot.study.detail.navigation.navigateToStudyDetail import com.umcspot.spot.study.detail.navigation.studyDetailGraph import com.umcspot.spot.study.my.navigation.myStudyGraph +import com.umcspot.spot.study.preferCategory.navigation.navigateToPreferCategoryStudy +import com.umcspot.spot.study.preferCategory.navigation.navigateToPreferCategoryStudyFilter +import com.umcspot.spot.study.preferCategory.navigation.preferCategoryStudyFilterGraph +import com.umcspot.spot.study.preferCategory.navigation.preferCategoryStudyGraph import com.umcspot.spot.study.preferLocation.navigation.preferLocationStudyFilterGraph import com.umcspot.spot.study.preferLocation.navigation.preferLocationStudyGraph import com.umcspot.spot.study.recruiting.navigation.recruitingStudyFilterGraph @@ -78,9 +82,7 @@ fun MainNavHost( when (type) { QuickMenuType.BOARD -> navigator.navigateToBoard() QuickMenuType.REGION -> navigator.navigateToPreferLocationStudy() - QuickMenuType.INTERESTS -> { /* TODO */ - } - + QuickMenuType.INTERESTS -> navigator.navController.navigateToPreferCategoryStudy() QuickMenuType.RECRUITING -> navigator.navigateToRecruitingStudy() } }, @@ -168,6 +170,19 @@ fun MainNavHost( onAcceptFilterClick = { navigator.popBackStack() } ) + preferCategoryStudyGraph( + contentPadding = contentPadding, + onRegisterScrollToTop = onRegisterScrollToTop, + onItemClick = { }, + onFilterClick = { navigator.navController.navigateToPreferCategoryStudyFilter() }, + ) + + preferCategoryStudyFilterGraph( + contentPadding = contentPadding, + navController = navigator.navController, + onAcceptFilterClick = { navigator.popBackStack() } + ) + boardGraph( contentPadding = contentPadding, navController = navigator.navController, diff --git a/feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt b/feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt index 1e6d7715..0153a8ed 100644 --- a/feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt +++ b/feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt @@ -36,6 +36,8 @@ import com.umcspot.spot.signup.navigation.navigateToSaving import com.umcspot.spot.signup.navigation.navigateToSignUp import com.umcspot.spot.study.detail.navigation.navigateToStudyDetail import com.umcspot.spot.study.my.navigation.navigateToMyStudy +import com.umcspot.spot.study.preferCategory.navigation.PreferCategory +import com.umcspot.spot.study.preferCategory.navigation.PreferCategoryFilter import com.umcspot.spot.study.preferLocation.navigation.PreferLocation import com.umcspot.spot.study.preferLocation.navigation.PreferLocationFilter import com.umcspot.spot.study.preferLocation.navigation.navigateToPreferLocationStudy @@ -99,14 +101,14 @@ class MainNavigator( fun isInLanding(): Boolean = inAnyGraph(Landing::class, Saving::class) @Composable - fun showBackTopBar(): Boolean = inAnyGraph(Alert::class, RecruitingFilter::class, PreferLocationFilter::class, + fun showBackTopBar(): Boolean = inAnyGraph(Alert::class, RecruitingFilter::class, PreferLocationFilter::class, PreferCategoryFilter::class, SignUp::class, CheckList::class, Posting::class, BoardList::class, JJim::class, MyPage::class, ParticipatingStudy::class, MyRecruitingStudy::class, WaitingStudy::class ) || inAnyGraphRoutes(POST_CONTENT_ROUTE) @Composable fun showToTopFab(): Boolean = inAnyGraph(Alert::class, Recruiting::class, - PreferLocation::class, BoardList::class, JJim::class, ParticipatingStudy::class, MyRecruitingStudy::class, WaitingStudy::class) + PreferLocation::class, PreferCategory::class, BoardList::class, JJim::class, ParticipatingStudy::class, MyRecruitingStudy::class, WaitingStudy::class) @Composable fun showMultipleFab(): Boolean = inAnyGraph(Home::class, BoardList::class) diff --git a/feature/main/src/main/java/com/umcspot/spot/main/MainScreen.kt b/feature/main/src/main/java/com/umcspot/spot/main/MainScreen.kt index 1d183e1e..055fd6a0 100644 --- a/feature/main/src/main/java/com/umcspot/spot/main/MainScreen.kt +++ b/feature/main/src/main/java/com/umcspot/spot/main/MainScreen.kt @@ -42,6 +42,7 @@ import com.umcspot.spot.mypage.waiting.navigation.WaitingStudy import com.umcspot.spot.signup.navigation.CheckList import com.umcspot.spot.signup.navigation.SignUp import com.umcspot.spot.study.detail.navigation.StudyDetail +import com.umcspot.spot.study.preferCategory.navigation.PreferCategoryFilter import com.umcspot.spot.study.preferLocation.navigation.PreferLocationFilter import com.umcspot.spot.study.recruiting.navigation.RecruitingFilter import com.umcspot.spot.study.register.navigation.RegisterStudy @@ -70,6 +71,7 @@ fun MainScreen( dest?.hasRoute(Alert::class) == true -> "알림" dest?.hasRoute(RecruitingFilter::class) == true -> "모집중인 스터디" dest?.hasRoute(PreferLocationFilter::class) == true -> "내 지역 스터디" + dest?.hasRoute(PreferCategoryFilter::class) == true -> "내 관심사 스터디" dest?.hasRoute(SignUp::class) == true -> "회원가입" dest?.hasRoute(CheckList::class) == true -> "체크리스트" dest?.hasRoute(Posting::class) == true -> "글쓰기" diff --git a/feature/mypage/src/main/java/com/umcspot/spot/mypage/main/MyPageViewModel.kt b/feature/mypage/src/main/java/com/umcspot/spot/mypage/main/MyPageViewModel.kt index 63efb1ff..5f624c23 100644 --- a/feature/mypage/src/main/java/com/umcspot/spot/mypage/main/MyPageViewModel.kt +++ b/feature/mypage/src/main/java/com/umcspot/spot/mypage/main/MyPageViewModel.kt @@ -65,7 +65,8 @@ class MyPageViewModel @Inject constructor( viewModelScope.launch { userRepository.getUserPreferredCategory() .onSuccess { info -> - _uiState.update { it.copy(preferCategories = UiState.Success(info.categories)) } + val titles = info.categories.mapNotNull { it?.title } + _uiState.update { it.copy(preferCategories = UiState.Success(titles)) } } .onFailure { e -> Log.e("HomeViewModel", "loadPreferCategories error", e) diff --git a/feature/study/src/main/java/com/umcspot/spot/study/preferCategory/PreferCategoryStudyFilterScreen.kt b/feature/study/src/main/java/com/umcspot/spot/study/preferCategory/PreferCategoryStudyFilterScreen.kt new file mode 100644 index 00000000..dd360a9f --- /dev/null +++ b/feature/study/src/main/java/com/umcspot/spot/study/preferCategory/PreferCategoryStudyFilterScreen.kt @@ -0,0 +1,318 @@ +package com.umcspot.spot.study.preferCategory + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +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.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.umcspot.spot.designsystem.R +import com.umcspot.spot.designsystem.component.button.MultiButton +import com.umcspot.spot.designsystem.component.button.TextButton +import com.umcspot.spot.designsystem.component.button.TextButtonState +import com.umcspot.spot.designsystem.shapes.SpotShapes +import com.umcspot.spot.designsystem.theme.SpotTheme +import com.umcspot.spot.model.ActivityType +import com.umcspot.spot.model.FeeRange +import com.umcspot.spot.model.RecruitingStatus +import com.umcspot.spot.ui.extension.screenHeightDp +import com.umcspot.spot.ui.extension.screenWidthDp + +@Composable +fun PreferCategoryStudyFilterScreen( + contentPadding: PaddingValues, + onAcceptFilterClick: () -> Unit, + categoryVm: PreferCategoryStudyViewModel = hiltViewModel(), +) { + val recruitingStatus by categoryVm.recruitingStatus.collectAsStateWithLifecycle() + val fee by categoryVm.feeRange.collectAsStateWithLifecycle() + val activity by categoryVm.activity.collectAsStateWithLifecycle() + + var draftRecruitingStatus by rememberSaveable { mutableStateOf(recruitingStatus) } + var draftFee by rememberSaveable { mutableStateOf(fee) } + var draftActivity by rememberSaveable { mutableStateOf(activity) } + + val acceptEnabled = true + + val topPad = contentPadding.calculateTopPadding() + val bottomPad = contentPadding.calculateBottomPadding() + + BackHandler { + onAcceptFilterClick() + } + + PreferCategoryStudyFilterScreenContent( + modifier = Modifier + .background(SpotTheme.colors.white) + .padding(top = topPad, bottom = bottomPad), + recruitingStatus = draftRecruitingStatus, + fee = draftFee, + activity = draftActivity, + buttonEnabled = acceptEnabled, + onToggleRecruitingStatus = { type -> + draftRecruitingStatus = if (draftRecruitingStatus == type) null else type + }, + onToggleFee = { fee -> draftFee = if (draftFee == fee) null else fee }, + onToggleActivity = { activity -> draftActivity = if (draftActivity == activity) null else activity }, + onReset = { + draftRecruitingStatus = null + draftFee = null + draftActivity = null + }, + onApply = { + categoryVm.applyFilter( + fee = draftFee, + recruitingStatus = draftRecruitingStatus, + activityType = draftActivity + ) + onAcceptFilterClick() + } + ) +} + +@Composable +fun PreferCategoryStudyFilterScreenContent( + recruitingStatus: RecruitingStatus?, + fee: FeeRange?, + activity: ActivityType?, + buttonEnabled: Boolean, + onToggleRecruitingStatus: (RecruitingStatus) -> Unit, + onToggleFee: (FeeRange) -> Unit, + onToggleActivity: (ActivityType) -> Unit, + onReset: () -> Unit, + onApply: () -> Unit, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .fillMaxSize() + ) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.Top + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(screenHeightDp(25.dp)) + .background(SpotTheme.colors.gray100), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "검색 결과는 모든 관심사에 공통 반영됩니다.", + style = SpotTheme.typography.small_500, + color = SpotTheme.colors.black + ) + } + + Column( + modifier = Modifier + .padding(top = screenHeightDp(18.dp)) + .padding(horizontal = screenWidthDp(17.dp)) + ) { + Text( + text = "모집 상태", + style = SpotTheme.typography.h5, + color = SpotTheme.colors.black + ) + + Spacer(modifier = Modifier.height(screenHeightDp(20.dp))) + + RecruitingStatusMultiSection( + recruitingStatus = recruitingStatus, + onSelect = onToggleRecruitingStatus + ) + + Spacer(modifier = Modifier.height(screenHeightDp(53.dp))) + + Text( + text = "활동", + style = SpotTheme.typography.h5, + color = SpotTheme.colors.black + ) + + Spacer(modifier = Modifier.height(screenHeightDp(20.dp))) + + + ActivityTypeMultiSection( + selectedTypes = activity, + onToggle = onToggleActivity + ) + + Spacer(modifier = Modifier.height(screenHeightDp(53.dp))) + + Text( + text = "활동비", + style = SpotTheme.typography.h5, + color = SpotTheme.colors.black + ) + + Spacer(modifier = Modifier.height(screenHeightDp(20.dp))) + + ActivityFeeSection( + activityFee = fee, + onSelect = onToggleFee + ) + + Spacer(modifier = Modifier.height(screenHeightDp(33.dp))) + + ResetFilterText( + onClick = onReset + ) + + Spacer(Modifier.height(80.dp)) + } + } + + Box( + modifier = Modifier + .align(alignment = Alignment.BottomCenter) + .fillMaxWidth() + .navigationBarsPadding() + .padding(horizontal = screenWidthDp(16.dp), vertical = screenHeightDp(12.dp)) + .zIndex(1f) + ) { + TextButton( + text = "검색 결과 보기", + modifier = Modifier + .width(screenWidthDp(326.dp)) + .height(screenHeightDp(47.dp)), + enabled = buttonEnabled, + onClick = onApply + ) + } + } +} + +@Composable +fun RecruitingStatusMultiSection( + recruitingStatus: RecruitingStatus?, + onSelect: (RecruitingStatus) -> Unit +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(screenWidthDp(14.dp)) + ) { + RecruitingStatus.entries.forEach { type -> + TextButton( + modifier = Modifier + .width(screenWidthDp(71.dp)) + .height(screenHeightDp(35.dp)), + text = type.value, + shape = SpotShapes.Hard, + state = TextButtonState.Toggle, + checked = (recruitingStatus == type), + onClick = { onSelect(type) }, + style = SpotTheme.typography.medium_500 + ) + } + } +} + +@Composable +fun ActivityTypeMultiSection( + selectedTypes: ActivityType?, + onToggle: (ActivityType) -> Unit +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(screenWidthDp(14.dp)) + ) { + ActivityType.entries.forEach { type -> + MultiButton( + modifier = Modifier.weight(1f), + text = type.label, + shape = SpotShapes.Soft, + painter = getIconForType(type), + checked = (selectedTypes == type), + onClick = { onToggle(type) } + ) + } + } +} + +@Composable +fun ActivityFeeSection( + activityFee: FeeRange?, + onSelect: (FeeRange) -> Unit +) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(screenWidthDp(14.dp)), + verticalArrangement = Arrangement.spacedBy(screenHeightDp(14.dp)) + ) { + FeeRange.entries.forEach { fee -> + TextButton( + text = fee.label, + modifier = Modifier + .width(screenWidthDp(71.dp)) + .height(screenHeightDp(35.dp)), + state = TextButtonState.Toggle, + checked = activityFee == fee, + onClick = { onSelect(fee) }, + shape = SpotShapes.Hard, + style = SpotTheme.typography.medium_500 + ) + } + } +} + +@Composable +fun ResetFilterText( + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Text( + text = "필터 초기화", + color = SpotTheme.colors.gray500, + style = SpotTheme.typography.regular_500, + textDecoration = TextDecoration.Underline, + modifier = modifier + .semantics { role = Role.Button } + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onClick + ) + ) +} + +@Composable +private fun getIconForType(type: ActivityType) = when (type) { + ActivityType.ONLINE -> painterResource(R.drawable.online) + ActivityType.OFFLINE -> painterResource(R.drawable.offline) +} + diff --git a/feature/study/src/main/java/com/umcspot/spot/study/preferCategory/PreferCategoryStudyScreen.kt b/feature/study/src/main/java/com/umcspot/spot/study/preferCategory/PreferCategoryStudyScreen.kt new file mode 100644 index 00000000..1fba0653 --- /dev/null +++ b/feature/study/src/main/java/com/umcspot/spot/study/preferCategory/PreferCategoryStudyScreen.kt @@ -0,0 +1,479 @@ +package com.umcspot.spot.study.preferCategory + +import androidx.compose.foundation.BorderStroke +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 +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentWidth +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.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.ScrollableTabRow +import androidx.compose.material3.Surface +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +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.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.umcspot.spot.designsystem.R +import com.umcspot.spot.designsystem.component.SpotSpinner +import com.umcspot.spot.designsystem.component.empty.EmptyAlert +import com.umcspot.spot.designsystem.component.study.StudyListItem +import com.umcspot.spot.designsystem.shapes.SpotShapes +import com.umcspot.spot.designsystem.theme.B100 +import com.umcspot.spot.designsystem.theme.B500 +import com.umcspot.spot.designsystem.theme.G200 +import com.umcspot.spot.designsystem.theme.G300 +import com.umcspot.spot.designsystem.theme.SpotTheme +import com.umcspot.spot.model.RecruitingStudySort +import com.umcspot.spot.model.StudyTheme +import com.umcspot.spot.study.model.StudyResult +import com.umcspot.spot.ui.extension.screenHeightDp +import com.umcspot.spot.ui.extension.screenWidthDp +import com.umcspot.spot.ui.state.UiState +import kotlinx.coroutines.launch +import kotlin.collections.lastIndex +import kotlin.collections.orEmpty +import kotlin.text.format + +@Composable +fun PreferCategoryStudyScreen( + contentPadding: PaddingValues, + viewmodel: PreferCategoryStudyViewModel = hiltViewModel(), + onRegisterScrollToTop: ((() -> Unit)?) -> Unit, + onFilterClick: () -> Unit, + onItemClick: (Long) -> Unit +) { + val ui by viewmodel.uiState.collectAsStateWithLifecycle() + val sort by viewmodel.sortType.collectAsStateWithLifecycle() + val isLoadingMore by viewmodel.isLoadingMore.collectAsStateWithLifecycle() + val selectedTab by viewmodel.selectedTab.collectAsStateWithLifecycle() + val preferCategories by viewmodel.preferCategory.collectAsStateWithLifecycle() + + + var showSortSheet by remember { mutableStateOf(false) } + + val listState = rememberLazyListState() + val scope = rememberCoroutineScope() + + val topPad = contentPadding.calculateTopPadding() + val bottomPad = contentPadding.calculateBottomPadding() + + val isFiltered by viewmodel.isFiltered.collectAsStateWithLifecycle() + + // 상단 스크롤 콜백 등록 + LaunchedEffect(Unit) { + viewmodel.getPreferCategories() + viewmodel.load() + onRegisterScrollToTop { + scope.launch { listState.animateScrollToItem(0) } + } + } + + val isSuccess = ui.data is UiState.Success + val studiesForUi = (ui.data as? UiState.Success)?.data?.studyList.orEmpty() + LaunchedEffect(isSuccess, studiesForUi.size, isLoadingMore) { + if (!isSuccess) return@LaunchedEffect + snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index } + .collect { lastVisible -> + val total = listState.layoutInfo.totalItemsCount + if (lastVisible != null && total > 0 && lastVisible >= total - 3) { + viewmodel.loadNextPage() + } + } + } + + val allTabs: List = listOf(null) + preferCategories + val selectedIndex = allTabs.indexOfFirst { it == selectedTab }.coerceAtLeast(0) + + Column( + modifier = Modifier + .fillMaxSize() + .background(SpotTheme.colors.white) + .padding(top = topPad, bottom = bottomPad) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = screenHeightDp(9.dp), horizontal = screenWidthDp(17.dp)), + verticalAlignment = Alignment.CenterVertically + ) { + // 타이틀 + Text( + text = "내 관심사 스터디", + style = SpotTheme.typography.h4 + ) + } + + CategoryTabs( + tabs = allTabs, + selectedIndex = selectedIndex, + onTabSelected = { category -> + viewmodel.setSelectedTab(category) + scope.launch { listState.animateScrollToItem(0) } + } + ) + + Spacer(Modifier.height(screenHeightDp(12.dp))) + + HeaderRow( + size = studiesForUi.size, + sortType = sort, + onOpenSortSheet = { showSortSheet = true }, + onFilterClick = onFilterClick, + isFiltered = isFiltered + ) + + Box( + modifier = Modifier + .fillMaxSize() + ) { + when (val state = ui.data) { + is UiState.Success -> { + StudyList( + listState = listState, + items = state.data.studyList, + onItemClick = onItemClick + ) + } + + is UiState.Loading -> { + Surface( + color = SpotTheme.colors.white, + modifier = Modifier + .fillMaxSize() + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + SpotSpinner() + } + } + } + + is UiState.Empty, is UiState.Failure -> { + EmptyAlert( + modifier = Modifier.fillMaxSize(), + painter = painterResource(R.drawable.emoji_sad), + alertTitle = "조건에 맞는 스터디가 없어요.", + alertDes = "필터를 재설정하고 스터디를 찾아보세요.", + ) + } + } + } + } + + SortTypeBottomSheet( + visible = showSortSheet, + current = sort, + onSelect = { viewmodel.setSort(it) }, + onDismiss = { showSortSheet = false } + ) +} + +@Composable +private fun StudyList( + listState: LazyListState, + items: List, + onItemClick: (Long) -> Unit +) { + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxSize() + .padding(horizontal = screenWidthDp(17.dp)) + ) { + items( + items = items, + key = { it.id } + ) { item -> + Spacer(Modifier.padding(screenHeightDp(5.dp))) + + StudyListItem( + item = item, + modifier = Modifier + .fillMaxWidth(), + onClick = { onItemClick(item.id) } + ) + + if(items.indexOf(item) != items.lastIndex) { + Spacer(Modifier.padding(screenHeightDp(5.dp))) + + HorizontalDivider( + modifier = Modifier + .fillMaxWidth(), + color = SpotTheme.colors.G300, + thickness = 1.dp + ) + } + } + } +} + +@Composable +private fun CategoryTabs( + tabs: List, + selectedIndex: Int, + onTabSelected: (StudyTheme?) -> Unit +) { + if (tabs.isEmpty()) return + + val scrimWidth = screenWidthDp(24.dp) + val bg = SpotTheme.colors.white + + Box( + modifier = Modifier + .fillMaxWidth() + .drawWithContent { + drawContent() + + val w = scrimWidth.toPx() + // 왼쪽: 내부(흰색) → 바깥(투명) + drawRect( + brush = Brush.horizontalGradient( + colors = listOf(Color.Transparent, bg), + startX = w, + endX = 0f + ), + size = Size(w, size.height), + topLeft = Offset(0f, 0f) + ) + // 오른쪽: 내부(투명) → 바깥(흰색) + drawRect( + brush = Brush.horizontalGradient( + colors = listOf(Color.Transparent, bg), + startX = size.width - w, + endX = size.width + ), + size = Size(w, size.height), + topLeft = Offset(size.width - w, 0f) + ) + } + ) { + ScrollableTabRow( + modifier = Modifier.fillMaxWidth(), + selectedTabIndex = selectedIndex, + edgePadding = 0.dp, + containerColor = Color.Transparent, + divider = { + HorizontalDivider( + thickness = 1.dp, + color = SpotTheme.colors.gray200 + ) + }, + indicator = { tabPositions -> + TabRowDefaults.SecondaryIndicator( + modifier = Modifier + .tabIndicatorOffset(tabPositions[selectedIndex]) + .padding(horizontal = screenWidthDp(17.dp)) + .height(1.dp), + color = SpotTheme.colors.B500 + ) + } + ) { + tabs.forEachIndexed { index, theme -> + + Tab( + modifier = Modifier + .wrapContentWidth(), + selected = selectedIndex == index, + onClick = { onTabSelected(theme) }, + selectedContentColor = SpotTheme.colors.black, + unselectedContentColor = SpotTheme.colors.black + ) { + Text( + text = theme?.title ?: "전체", + style = SpotTheme.typography.h5, + modifier = Modifier + .padding(horizontal = screenWidthDp(7.dp), vertical = screenHeightDp(4.dp)) + ) + } + } + } + } +} + +@Composable +fun HeaderRow( + size: Int, + sortType: RecruitingStudySort, + onOpenSortSheet: () -> Unit, + onFilterClick: () -> Unit, + isFiltered: Boolean +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = screenWidthDp(17.dp)), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "%02d건".format(size), + style = SpotTheme.typography.regular_500, + color = SpotTheme.colors.gray400 + ) + + Row{ + OutlinedButton( + onClick = onOpenSortSheet, + shape = SpotShapes.Hard, + border = BorderStroke(1.dp, SpotTheme.colors.G200), + contentPadding = PaddingValues(start = screenWidthDp(7.dp), end = screenWidthDp(4.dp), top = screenHeightDp(2.dp), bottom = screenHeightDp(4.dp)), + modifier = Modifier + .wrapContentWidth() + .height(screenHeightDp(26.dp)) + ) { + Text( + text = sortType.label, + color = SpotTheme.colors.black, + style = SpotTheme.typography.regular_500 + ) + Spacer(Modifier.width(screenWidthDp(7.dp))) + Icon( + modifier = Modifier.size(screenWidthDp(14.dp)), + painter = painterResource(R.drawable.arrow_down), + tint = SpotTheme.colors.B500, + contentDescription = null + ) + } + + Spacer(Modifier.width(screenWidthDp(10.dp))) + + Box( + modifier = Modifier + .size(screenWidthDp(26.dp)) + .clip(SpotShapes.Hard) + .background( + color = if (isFiltered) SpotTheme.colors.B100 else SpotTheme.colors.white + ) + .clickable( + onClick = onFilterClick + ), + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(R.drawable.filter), + contentDescription = "필터", + modifier = Modifier.size(screenWidthDp(14.dp)), + tint = if (isFiltered) SpotTheme.colors.B500 else SpotTheme.colors.black + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SortTypeBottomSheet( + visible: Boolean, + current: RecruitingStudySort?, + onSelect: (RecruitingStudySort) -> Unit, + onDismiss: () -> Unit +) { + if(!visible) return + + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + ModalBottomSheet( + modifier = Modifier + .fillMaxWidth(), + onDismissRequest = onDismiss, + sheetState = sheetState, + shape = SpotShapes.RoundTop, + containerColor = SpotTheme.colors.white, + dragHandle = { }, + contentWindowInsets = { WindowInsets(0) }, + tonalElevation = 0.dp + ) { + Column( + modifier = Modifier + .navigationBarsPadding() + .padding(vertical = screenHeightDp(14.dp)) + ) { + RecruitingStudySort.entries.forEachIndexed { index, option -> + ListItem( + colors = ListItemDefaults.colors( + containerColor = SpotTheme.colors.white, + headlineColor = SpotTheme.colors.black, + trailingIconColor = SpotTheme.colors.B500 + ), + headlineContent = { + Text( + modifier = Modifier, + text = option.label, + color = SpotTheme.colors.black, + style = SpotTheme.typography.medium_400 + ) + }, + trailingContent = { + if (option == current) { + Icon( + painter = painterResource(R.drawable.success_default), + tint = SpotTheme.colors.B500, + modifier = Modifier + .size(screenWidthDp(14.dp)), + contentDescription = "선택됨", + ) + } + }, + modifier = Modifier + .fillMaxWidth() + .clickable { + onSelect(option) + onDismiss() + } + .padding(horizontal = screenWidthDp(17.dp)) + ) + if (index != RecruitingStudySort.entries.lastIndex) { + HorizontalDivider( + color = SpotTheme.colors.G300, + thickness = 1.dp + ) + } + } + } + } +} \ No newline at end of file diff --git a/feature/study/src/main/java/com/umcspot/spot/study/preferCategory/PreferCategoryStudyViewModel.kt b/feature/study/src/main/java/com/umcspot/spot/study/preferCategory/PreferCategoryStudyViewModel.kt new file mode 100644 index 00000000..25ecb529 --- /dev/null +++ b/feature/study/src/main/java/com/umcspot/spot/study/preferCategory/PreferCategoryStudyViewModel.kt @@ -0,0 +1,167 @@ +package com.umcspot.spot.study.preferCategory + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.umcspot.spot.common.location.LocationStore +import com.umcspot.spot.model.ActivityType +import com.umcspot.spot.model.FeeRange +import com.umcspot.spot.model.RecruitingStatus +import com.umcspot.spot.model.RecruitingStudySort +import com.umcspot.spot.model.StudyTheme +import com.umcspot.spot.study.model.StudyResultList +import com.umcspot.spot.study.repository.StudyRepository +import com.umcspot.spot.ui.state.UiState +import com.umcspot.spot.user.repository.UserRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class PreferCategoryStudyViewModel @Inject constructor( + private val studyRepository: StudyRepository, + private val userRepository: UserRepository +) : ViewModel() { + data class ScrollPosition(val index: Int = 0, val offset: Int = 0) + data class PreferCategoryUiState(val data: UiState = UiState.Empty) + + var scrollPosition: ScrollPosition = ScrollPosition() + + private val _uiState = MutableStateFlow(PreferCategoryUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _preferCategory = MutableStateFlow>(emptyList()) + val preferCategory: StateFlow> = _preferCategory.asStateFlow() + + private val _selectedTab = MutableStateFlow(null) + val selectedTab: StateFlow = _selectedTab.asStateFlow() + + private val _recruitingStatus = MutableStateFlow(null) + val recruitingStatus: StateFlow = _recruitingStatus.asStateFlow() + + private val _feeRange = MutableStateFlow(null) + val feeRange: StateFlow = _feeRange.asStateFlow() + + private val _activity = MutableStateFlow(null) + val activity: StateFlow = _activity.asStateFlow() + + private val _sortType = MutableStateFlow(RecruitingStudySort.RECENT) + val sortType: StateFlow = _sortType.asStateFlow() + + /** 로딩 중 페이징 플래그 */ + private val _isLoadingMore = MutableStateFlow(false) + val isLoadingMore: StateFlow = _isLoadingMore.asStateFlow() + + val isFiltered: StateFlow = + combine(_recruitingStatus, _feeRange, _activity) { status, fee, activity -> + status != null || fee != null || activity != null + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = false + ) + + /** ========== 최초/새로고침 로드 ========== */ + + fun getPreferCategories() { + viewModelScope.launch { + userRepository.getUserPreferredCategory() + .onSuccess { info -> + _preferCategory.value = info.categories + } + } + } + + fun load() { + viewModelScope.launch { + runCatching { + _uiState.update { it.copy(data = UiState.Loading) } + + studyRepository.getPreferCategoryStudies( + category = _selectedTab.value, + recruitingStatus = _recruitingStatus.value, + feeRange = _feeRange.value, + isOnline = _activity.value.toIsOnline(), + sortBy = _sortType.value, + cursor = null, + size = 20, + ).getOrThrow() + }.onSuccess { firstPage -> + _uiState.update { + it.copy( + data = if (firstPage.studyList.isEmpty()) + UiState.Empty + else + UiState.Success(firstPage) + ) + } + }.onFailure { e -> + Log.e("PreferCategoryStudyViewModel", "load error", e) + _uiState.update { it.copy(data = UiState.Empty) } + } + } + } + + fun loadNextPage() { + val current = (_uiState.value.data as? UiState.Success)?.data ?: return + if (!current.hasNext || _isLoadingMore.value) return + + viewModelScope.launch { + _isLoadingMore.value = true + + runCatching { + studyRepository.getPreferCategoryStudies( + category = _selectedTab.value, + recruitingStatus = _recruitingStatus.value, + feeRange = _feeRange.value, + isOnline = _activity.value.toIsOnline(), + sortBy = _sortType.value, + cursor = current.nextCursor, + size = 20 + ).getOrThrow() + }.onSuccess { newPage -> + val merged = current.copy( + studyList = current.studyList + newPage.studyList, + hasNext = newPage.hasNext, + nextCursor = newPage.nextCursor + ) + _uiState.update { it.copy(data = UiState.Success(merged)) } + }.onFailure { e -> + Log.e("PreferCategoryStudyViewModel", "loadNextPage error", e) + } + + _isLoadingMore.value = false + } + } + + fun applyFilter(recruitingStatus: RecruitingStatus?, fee: FeeRange?, activityType: ActivityType?) { + _recruitingStatus.value = recruitingStatus + _feeRange.value = fee + _activity.value = activityType + load() + } + + fun setSort(sort: RecruitingStudySort) { + _sortType.value = sort + load() + } + + fun setSelectedTab(theme: StudyTheme?) { + _selectedTab.value = theme + load() + } + + + private fun ActivityType?.toIsOnline(): Boolean? = when (this) { + ActivityType.ONLINE -> true + ActivityType.OFFLINE -> false + null -> null + } +} \ No newline at end of file diff --git a/feature/study/src/main/java/com/umcspot/spot/study/preferCategory/navigation/PreferCategoryStudyFilterNavigation.kt b/feature/study/src/main/java/com/umcspot/spot/study/preferCategory/navigation/PreferCategoryStudyFilterNavigation.kt new file mode 100644 index 00000000..5b6688a4 --- /dev/null +++ b/feature/study/src/main/java/com/umcspot/spot/study/preferCategory/navigation/PreferCategoryStudyFilterNavigation.kt @@ -0,0 +1,39 @@ +package com.umcspot.spot.study.preferCategory.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.remember +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.umcspot.spot.navigation.MainTabRoute +import com.umcspot.spot.study.preferCategory.PreferCategoryStudyFilterScreen +import com.umcspot.spot.study.preferCategory.PreferCategoryStudyViewModel +import kotlinx.serialization.Serializable + +fun NavController.navigateToPreferCategoryStudyFilter(navOptions: NavOptions? = null) { + navigate(PreferCategoryFilter, navOptions) +} + +fun NavGraphBuilder.preferCategoryStudyFilterGraph( + navController: NavController, + contentPadding: PaddingValues, + onAcceptFilterClick: () -> Unit, +) { + composable { + val parentEntry = remember(navController) { + navController.getBackStackEntry(PreferCategory) + } + + val categoryVm: PreferCategoryStudyViewModel = hiltViewModel(parentEntry) + PreferCategoryStudyFilterScreen( + contentPadding = contentPadding, + categoryVm = categoryVm, + onAcceptFilterClick = onAcceptFilterClick + ) + } +} + +@Serializable +data object PreferCategoryFilter : MainTabRoute \ No newline at end of file diff --git a/feature/study/src/main/java/com/umcspot/spot/study/preferCategory/navigation/PreferCategoryStudyNavigation.kt b/feature/study/src/main/java/com/umcspot/spot/study/preferCategory/navigation/PreferCategoryStudyNavigation.kt new file mode 100644 index 00000000..96b45557 --- /dev/null +++ b/feature/study/src/main/java/com/umcspot/spot/study/preferCategory/navigation/PreferCategoryStudyNavigation.kt @@ -0,0 +1,33 @@ +package com.umcspot.spot.study.preferCategory.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.umcspot.spot.navigation.MainTabRoute +import com.umcspot.spot.study.preferCategory.PreferCategoryStudyScreen +import kotlinx.serialization.Serializable + +fun NavController.navigateToPreferCategoryStudy(navOptions: NavOptions? = null) { + navigate(PreferCategory, navOptions) +} + +fun NavGraphBuilder.preferCategoryStudyGraph( + contentPadding: PaddingValues, + onRegisterScrollToTop: ((() -> Unit)?) -> Unit, + onItemClick: (Long) -> Unit, + onFilterClick: () -> Unit +) { + composable { + PreferCategoryStudyScreen( + contentPadding = contentPadding, + onRegisterScrollToTop = onRegisterScrollToTop, + onItemClick = onItemClick, + onFilterClick = onFilterClick + ) + } +} + +@Serializable +data object PreferCategory : MainTabRoute \ No newline at end of file