diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b881960d..a150919e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -72,7 +72,14 @@ dependencies { implementation(project(":core:ui")) implementation(project(":data")) implementation(project(":domain")) + implementation(project(":feature:addlink")) + implementation(project(":feature:addpokit")) + implementation(project(":feature:alarm")) implementation(project(":feature:login")) + implementation(project(":feature:pokitdetail")) + implementation(project(":feature:search")) + implementation(project(":feature:settings")) + implementation(project(":feature:home")) // hilt implementation(libs.hilt) @@ -81,4 +88,13 @@ dependencies { // firebase implementation(platform(libs.firebase.bom)) implementation(libs.firebase.auth.ktx) + + // navigation + implementation(libs.androidx.navigation.compose) + implementation(libs.hilt.navigation.compose) + + // orbit + implementation(libs.orbit.compose) + implementation(libs.orbit.core) + implementation(libs.orbit.viewmodel) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 352408fa..4f0287c2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,7 +6,6 @@ + // 믹스패널/파베 애널리틱스 화면 이동 로깅용 + } + } + + RootNavHost(navHostController = navHostController) + } } } diff --git a/app/src/main/java/pokitmons/pokit/navigation/RootDestination.kt b/app/src/main/java/pokitmons/pokit/navigation/RootDestination.kt new file mode 100644 index 00000000..5b1f1436 --- /dev/null +++ b/app/src/main/java/pokitmons/pokit/navigation/RootDestination.kt @@ -0,0 +1,59 @@ +package pokitmons.pokit.navigation + +import androidx.navigation.NavType +import androidx.navigation.navArgument + +object Login { + val route: String = "login" +} + +object Home { + val route: String = "home" +} + +object AddLink { + val route: String = "addLink" + val linkIdArg = "link_id" + val routeWithArgs = "$route?$linkIdArg={$linkIdArg}" + var arguments = listOf( + navArgument(linkIdArg) { + nullable = true + type = NavType.StringType + } + ) +} + +object AddPokit { + val route: String = "addPokit" + val pokitIdArg = "pokit_id" + val routeWithArgs = "$route?$pokitIdArg={$pokitIdArg}" + var arguments = listOf( + navArgument(pokitIdArg) { + nullable = true + type = NavType.StringType + } + ) +} + +object PokitDetail { + val route: String = "pokitDetail" + val pokitIdArg = "pokit_id" + val routeWithArgs = "$route/{$pokitIdArg}" + var arguments = listOf(navArgument(pokitIdArg) { defaultValue = "-" }) +} + +object Search { + val route: String = "search" +} + +object Setting { + val route: String = "setting" +} + +object EditNickname { + val route: String = "editNickname" +} + +object Alarm { + val route: String = "alarm" +} diff --git a/app/src/main/java/pokitmons/pokit/navigation/RootNavHost.kt b/app/src/main/java/pokitmons/pokit/navigation/RootNavHost.kt new file mode 100644 index 00000000..13ce39b9 --- /dev/null +++ b/app/src/main/java/pokitmons/pokit/navigation/RootNavHost.kt @@ -0,0 +1,152 @@ +package pokitmons.pokit.navigation + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import com.strayalpaca.addlink.AddLinkScreenContainer +import com.strayalpaca.addlink.AddLinkViewModel +import com.strayalpaca.addpokit.AddPokitScreenContainer +import com.strayalpaca.addpokit.AddPokitViewModel +import com.strayalpaca.pokitdetail.PokitDetailScreenContainer +import com.strayalpaca.pokitdetail.PokitDetailViewModel +import pokitmons.pokit.LoginViewModel +import pokitmons.pokit.alarm.AlarmScreenContainer +import pokitmons.pokit.alarm.AlarmViewModel +import pokitmons.pokit.home.HomeScreen +import pokitmons.pokit.home.pokit.PokitViewModel +import pokitmons.pokit.login.LoginScreen +import pokitmons.pokit.search.SearchScreenContainer +import pokitmons.pokit.search.SearchViewModel +import pokitmons.pokit.settings.SettingViewModel +import pokitmons.pokit.settings.nickname.EditNicknameScreen +import pokitmons.pokit.settings.setting.SettingsScreen + +@Composable +fun RootNavHost( + navHostController: NavHostController, +) { + NavHost(navController = navHostController, startDestination = Home.route) { + composable(Login.route) { + val viewModel: LoginViewModel = hiltViewModel() + LoginScreen( + loginViewModel = viewModel, + onNavigateToTermsOfServiceScreen = {} + ) + } + + composable(Home.route) { + Box(modifier = Modifier.fillMaxSize()) + } + + composable( + route = AddLink.routeWithArgs, + arguments = AddLink.arguments + ) { + val viewModel: AddLinkViewModel = hiltViewModel() + AddLinkScreenContainer( + viewModel = viewModel, + onBackPressed = navHostController::popBackStack, + onNavigateToAddPokit = { + navHostController.navigate(AddPokit.route) + } + ) + } + + composable( + route = AddPokit.routeWithArgs, + arguments = AddPokit.arguments + ) { + val viewModel: AddPokitViewModel = hiltViewModel() + AddPokitScreenContainer( + viewModel = viewModel, + onBackPressed = navHostController::popBackStack, + onBackWithModifySuccess = { modifiedPokitId -> + navHostController.popBackStack() + navHostController.currentBackStackEntry + ?.savedStateHandle + ?.set("modified_pokit_id", modifiedPokitId) + } + ) + } + + composable( + route = PokitDetail.routeWithArgs, + arguments = PokitDetail.arguments + ) { + val viewModel: PokitDetailViewModel = hiltViewModel() + LaunchedEffect(it) { + val pokitId = navHostController.currentBackStackEntry?.savedStateHandle?.get("modified_pokit_id") ?: return@LaunchedEffect + viewModel.getPokit(pokitId) + } + + PokitDetailScreenContainer( + viewModel = viewModel, + onBackPressed = navHostController::popBackStack, + onNavigateToLinkModify = { linkId -> + navHostController.navigate("${AddLink.route}?${AddLink.linkIdArg}=$linkId") + }, + onNavigateToPokitModify = { pokitId -> + navHostController.navigate("${AddPokit.route}?${AddPokit.pokitIdArg}=$pokitId") + } + ) + } + + composable( + route = Search.route + ) { + val viewModel: SearchViewModel = hiltViewModel() + SearchScreenContainer( + viewModel = viewModel, + onBackPressed = navHostController::popBackStack, + onNavigateToLinkModify = { linkId -> + navHostController.navigate("${AddLink.route}?${AddLink.linkIdArg}=$linkId") + } + ) + } + + composable(route = Setting.route) { + val viewModel: SettingViewModel = hiltViewModel() + SettingsScreen( + settingViewModel = viewModel, + onNavigateToEditNickname = { navHostController.navigate(EditNickname.route) } + ) + } + + composable(route = EditNickname.route) { + val viewModel: SettingViewModel = hiltViewModel() + EditNicknameScreen( + settingViewModel = viewModel, + onBackPressed = navHostController::popBackStack + ) + } + + composable(route = Home.route) { + val viewModel: PokitViewModel = hiltViewModel() + HomeScreen( + viewModel = viewModel, + onNavigateToSearch = { navHostController.navigate(Search.route) }, + onNavigateToSetting = { navHostController.navigate(Setting.route) }, + onNavigateToPokitDetail = { navHostController.navigate("${PokitDetail.route}/$it") }, + onNavigateAddLink = { navHostController.navigate(AddLink.route) }, + onNavigateAddPokit = { navHostController.navigate(AddPokit.route) } + ) + } + + composable(route = Alarm.route) { + val viewModel: AlarmViewModel = hiltViewModel() + AlarmScreenContainer( + viewModel = viewModel, + onBackPressed = navHostController::popBackStack, + onNavigateToLinkModify = { linkId -> + navHostController.navigate("${AddLink.route}?${AddLink.linkIdArg}=$linkId") + } + ) + } + } +} diff --git a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/button/attributes/ButtonAttributes.kt b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/button/attributes/ButtonAttributes.kt index 6e97beee..f48ad11b 100644 --- a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/button/attributes/ButtonAttributes.kt +++ b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/button/attributes/ButtonAttributes.kt @@ -11,7 +11,7 @@ internal enum class PokitButtonState { } enum class PokitButtonStyle { - FILLED, STROKE, + FILLED, STROKE, DEFAULT } enum class PokitButtonSize { diff --git a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/button/subcomponents/container/PokitButtonContainerModifier.kt b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/button/subcomponents/container/PokitButtonContainerModifier.kt index 320ad4fe..41068313 100644 --- a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/button/subcomponents/container/PokitButtonContainerModifier.kt +++ b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/button/subcomponents/container/PokitButtonContainerModifier.kt @@ -89,6 +89,7 @@ private fun getBorderWidth( return when (style) { PokitButtonStyle.FILLED -> 0.dp PokitButtonStyle.STROKE -> 1.dp + PokitButtonStyle.DEFAULT -> 1.dp } } diff --git a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/button/subcomponents/icon/PokitButtonIcon.kt b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/button/subcomponents/icon/PokitButtonIcon.kt index 6e486960..5a0dd969 100644 --- a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/button/subcomponents/icon/PokitButtonIcon.kt +++ b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/button/subcomponents/icon/PokitButtonIcon.kt @@ -40,6 +40,10 @@ private fun getColor( style: PokitButtonStyle, ): Color { return when { + style == PokitButtonStyle.DEFAULT -> { + PokitTheme.colors.borderSecondary + } + state == PokitButtonState.DISABLE -> { PokitTheme.colors.iconDisable } diff --git a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/button/subcomponents/text/PokitButtonText.kt b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/button/subcomponents/text/PokitButtonText.kt index 6152138f..8efd82fd 100644 --- a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/button/subcomponents/text/PokitButtonText.kt +++ b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/button/subcomponents/text/PokitButtonText.kt @@ -61,6 +61,10 @@ private fun getColor( style: PokitButtonStyle, ): Color { return when { + style == PokitButtonStyle.DEFAULT -> { + PokitTheme.colors.textTertiary + } + state == PokitButtonState.DISABLE -> { PokitTheme.colors.textDisable } diff --git a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/block/linkcard/LinkCard.kt b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/block/linkcard/LinkCard.kt index 6d26555f..e318561c 100644 --- a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/block/linkcard/LinkCard.kt +++ b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/block/linkcard/LinkCard.kt @@ -107,7 +107,9 @@ fun LinkCard( Text( text = sub, - style = PokitTheme.typography.detail2.copy(color = PokitTheme.colors.textTertiary) + style = PokitTheme.typography.detail2.copy(color = PokitTheme.colors.textTertiary), + maxLines = 1, + overflow = TextOverflow.Ellipsis ) } diff --git a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/block/linkcard/Preview.kt b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/block/linkcard/Preview.kt index 9342c871..68b8616b 100644 --- a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/block/linkcard/Preview.kt +++ b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/block/linkcard/Preview.kt @@ -27,7 +27,7 @@ fun LinkCardPreview() { ) { LinkCard( title = "타이틀\n컴포스는 왜 이런가", - sub = "2024.06.25. youtube.com", + sub = "2024.06.25. youtube.comyoutube.comyoutube.comyoutube", badgeText = "텍스트", painter = painterResource(id = R.drawable.icon_24_link), notRead = true, diff --git a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/block/pokitcard/PokitCard.kt b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/block/pokitcard/PokitCard.kt index 8129a062..45b05417 100644 --- a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/block/pokitcard/PokitCard.kt +++ b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/block/pokitcard/PokitCard.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import pokitmons.pokit.core.ui.R import pokitmons.pokit.core.ui.theme.PokitTheme @@ -42,11 +43,11 @@ fun PokitCard( shape = RoundedCornerShape(8.dp) ) .background( - color = PokitTheme.colors.backgroundBase, + color = PokitTheme.colors.backgroundPrimary, shape = RoundedCornerShape(8.dp) ) .clickable(onClick = onClick) - .padding(all = 12.dp) + .padding(top = 12.dp, start = 12.dp, bottom = 8.dp, end = 8.dp) ) { Row( modifier = Modifier.fillMaxWidth() @@ -54,12 +55,13 @@ fun PokitCard( Text( modifier = Modifier.weight(1f), text = text, - style = PokitTheme.typography.body1Bold.copy(color = PokitTheme.colors.textPrimary), - maxLines = 2 + style = PokitTheme.typography.body1SemiBold.copy(color = PokitTheme.colors.textPrimary), + maxLines = 1, + overflow = TextOverflow.Ellipsis ) IconButton( - onClick = onClickKebab, + onClick = { onClickKebab() }, modifier = Modifier .size(24.dp) .align(Alignment.Top) @@ -83,13 +85,13 @@ fun PokitCard( Row( modifier = Modifier - .height(68.dp) + .height(84.dp) .fillMaxWidth(1f), horizontalArrangement = Arrangement.End ) { if (painter != null) { Image( - modifier = Modifier.size(68.dp), + modifier = Modifier.size(84.dp), contentScale = ContentScale.Crop, painter = painter, contentDescription = null diff --git a/core/ui/src/main/java/pokitmons/pokit/core/ui/theme/Theme.kt b/core/ui/src/main/java/pokitmons/pokit/core/ui/theme/Theme.kt index 27006d0d..ea2d0427 100644 --- a/core/ui/src/main/java/pokitmons/pokit/core/ui/theme/Theme.kt +++ b/core/ui/src/main/java/pokitmons/pokit/core/ui/theme/Theme.kt @@ -1,9 +1,14 @@ package pokitmons.pokit.core.ui.theme +import android.app.Activity import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat import pokitmons.pokit.core.ui.theme.color.LocalPokitColorDarkScheme import pokitmons.pokit.core.ui.theme.color.LocalPokitColorScheme import pokitmons.pokit.core.ui.theme.color.PokitColors @@ -31,6 +36,16 @@ fun PokitTheme( else -> LocalPokitColorScheme.current } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.backgroundBase.toArgb() + // dark theme 지원시 true 대신 !darkTheme를 할당하도록 수정 예정 + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = true + } + } + CompositionLocalProvider( LocalPokitColorScheme provides colorScheme, LocalPokitTypography provides typography diff --git a/core/ui/src/main/java/pokitmons/pokit/core/ui/theme/Type.kt b/core/ui/src/main/java/pokitmons/pokit/core/ui/theme/Type.kt index 6a178ce2..8fd5c4e5 100644 --- a/core/ui/src/main/java/pokitmons/pokit/core/ui/theme/Type.kt +++ b/core/ui/src/main/java/pokitmons/pokit/core/ui/theme/Type.kt @@ -36,6 +36,13 @@ data class PokitTypography( lineHeight = 24, letterSpacing = (-0.54f) ), + private val _body1SemiBold: PokitTypo = PokitTypo( + fontFamily = pretendard, + fontSize = 18, + fontWeight = FontWeight.SemiBold, + lineHeight = 24, + letterSpacing = (-0.54f) + ), private val _body1Medium: PokitTypo = PokitTypo( fontFamily = pretendard, fontSize = 18, @@ -139,6 +146,7 @@ data class PokitTypography( val title2: TextStyle @Composable get() = _title2.toDpTextStyle val title3: TextStyle @Composable get() = _title3.toDpTextStyle val body1Bold: TextStyle @Composable get() = _body1Bold.toDpTextStyle + val body1SemiBold: TextStyle @Composable get() = _body1SemiBold.toDpTextStyle val body1Medium: TextStyle @Composable get() = _body1Medium.toDpTextStyle val body2Bold: TextStyle @Composable get() = _body2Bold.toDpTextStyle val body2Medium: TextStyle @Composable get() = _body2Medium.toDpTextStyle diff --git a/core/ui/src/main/java/pokitmons/pokit/core/ui/theme/color/ColorSystem.kt b/core/ui/src/main/java/pokitmons/pokit/core/ui/theme/color/ColorSystem.kt index ad018eba..ba175484 100644 --- a/core/ui/src/main/java/pokitmons/pokit/core/ui/theme/color/ColorSystem.kt +++ b/core/ui/src/main/java/pokitmons/pokit/core/ui/theme/color/ColorSystem.kt @@ -24,7 +24,7 @@ data class PokitColors( val borderSecondary: Color = Gray200, val borderTertiary: Color = Gray100, val borderDisable: Color = Gray200, - val brand: Color = Orange500, + val brand: Color = Brand, val brandBold: Color = Orange700, val info: Color = Blue700, val warning: Color = Yellow400, diff --git a/core/ui/src/main/java/pokitmons/pokit/core/ui/theme/color/Colors.kt b/core/ui/src/main/java/pokitmons/pokit/core/ui/theme/color/Colors.kt index a18efd5a..f8cafcee 100644 --- a/core/ui/src/main/java/pokitmons/pokit/core/ui/theme/color/Colors.kt +++ b/core/ui/src/main/java/pokitmons/pokit/core/ui/theme/color/Colors.kt @@ -69,3 +69,5 @@ val Blue300 = Color(0xFF56BDFF) val Blue200 = Color(0xFF8DD0FF) val Blue100 = Color(0xFFBBE2FF) val Blue50 = Color(0xFFE3F1FF) + +val Brand = Color(0xFFFE8422) diff --git a/core/ui/src/main/res/drawable/icon_18_align.xml b/core/ui/src/main/res/drawable/icon_18_align.xml new file mode 100644 index 00000000..d0de91a0 --- /dev/null +++ b/core/ui/src/main/res/drawable/icon_18_align.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/icon_24_remind.xml b/core/ui/src/main/res/drawable/icon_24_remind.xml new file mode 100644 index 00000000..7d6f06bf --- /dev/null +++ b/core/ui/src/main/res/drawable/icon_24_remind.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/image_floating.xml b/core/ui/src/main/res/drawable/image_floating.xml new file mode 100644 index 00000000..f1183b4b --- /dev/null +++ b/core/ui/src/main/res/drawable/image_floating.xml @@ -0,0 +1,13 @@ + + + + diff --git a/core/ui/src/main/res/drawable/logo_pokit.xml b/core/ui/src/main/res/drawable/logo_pokit.xml new file mode 100644 index 00000000..fbdaba70 --- /dev/null +++ b/core/ui/src/main/res/drawable/logo_pokit.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/logo_remind.xml b/core/ui/src/main/res/drawable/logo_remind.xml new file mode 100644 index 00000000..25bcb6f7 --- /dev/null +++ b/core/ui/src/main/res/drawable/logo_remind.xml @@ -0,0 +1,9 @@ + + + diff --git a/data/build.gradle.kts b/data/build.gradle.kts index e9654d7c..08c375d2 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -49,6 +49,11 @@ dependencies { androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) + // room + implementation(libs.room.runtime) + annotationProcessor(libs.room.compiler) + kapt(libs.room.compiler) + // kotest testImplementation(libs.kotest.runner.junit5) testImplementation(libs.kotlin.reflect) @@ -71,4 +76,11 @@ dependencies { implementation(libs.androidx.datastore.preferences.core.jvm) implementation(project(":domain")) + + // jsoup + implementation(libs.jsoup) + + // mockk + testImplementation(libs.mockk) + androidTestImplementation(libs.mockk.android) } diff --git a/data/src/main/java/pokitmons/pokit/data/api/AlertApi.kt b/data/src/main/java/pokitmons/pokit/data/api/AlertApi.kt new file mode 100644 index 00000000..11c92782 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/api/AlertApi.kt @@ -0,0 +1,22 @@ +package pokitmons.pokit.data.api + +import pokitmons.pokit.data.model.alert.GetAlertsResponse +import pokitmons.pokit.domain.model.link.LinksSort +import retrofit2.http.GET +import retrofit2.http.PUT +import retrofit2.http.Path +import retrofit2.http.Query + +interface AlertApi { + @GET("alert") + suspend fun getAlerts( + @Query("page") page: Int, + @Query("size") size: Int, + @Query("sort") sort: List = listOf(LinksSort.RECENT.value), + ): GetAlertsResponse + + @PUT("alert/{alertId}") + suspend fun deleteAlert( + @Path("alertId") alertId: Int, + ) +} diff --git a/data/src/main/java/pokitmons/pokit/data/api/AuthApi.kt b/data/src/main/java/pokitmons/pokit/data/api/AuthApi.kt index 22fec397..57948752 100644 --- a/data/src/main/java/pokitmons/pokit/data/api/AuthApi.kt +++ b/data/src/main/java/pokitmons/pokit/data/api/AuthApi.kt @@ -1,8 +1,10 @@ package pokitmons.pokit.data.api import pokitmons.pokit.data.model.auth.request.SNSLoginRequest +import pokitmons.pokit.data.model.auth.request.SignUpRequest import pokitmons.pokit.data.model.auth.response.DuplicateNicknameResponse import pokitmons.pokit.data.model.auth.response.SNSLoginResponse +import pokitmons.pokit.data.model.auth.response.SignUpResponse import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST @@ -18,4 +20,9 @@ interface AuthApi { suspend fun checkDuplicateNickname( @Path(value = "nickname") nickname: String, ): DuplicateNicknameResponse + + @POST("user/signup") + suspend fun signUp( + @Body signUpRequest: SignUpRequest, + ): SignUpResponse } diff --git a/data/src/main/java/pokitmons/pokit/data/api/LinkApi.kt b/data/src/main/java/pokitmons/pokit/data/api/LinkApi.kt new file mode 100644 index 00000000..a7ac524f --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/api/LinkApi.kt @@ -0,0 +1,77 @@ +package pokitmons.pokit.data.api + +import pokitmons.pokit.data.model.link.request.ModifyLinkRequest +import pokitmons.pokit.data.model.link.response.ApplyBookmarkResponse +import pokitmons.pokit.data.model.link.response.GetLinkResponse +import pokitmons.pokit.data.model.link.response.GetLinksResponse +import pokitmons.pokit.data.model.link.response.ModifyLinkResponse +import pokitmons.pokit.domain.model.link.LinksSort +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.PATCH +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Path +import retrofit2.http.Query + +interface LinkApi { + @GET("content/{categoryId}") + suspend fun getLinks( + @Path("categoryId") categoryId: Int = 0, + @Query("page") page: Int = 0, + @Query("size") size: Int = 10, + @Query("sort") sort: List = listOf(LinksSort.RECENT.value), + @Query("isRead") isRead: Boolean = false, + @Query("favorites") favorites: Boolean = false, + @Query("startDate") startDate: String? = null, + @Query("endDate") endDate: String? = null, + @Query("categoryIds") categoryIds: List? = null, + ): GetLinksResponse + + @GET("content") + suspend fun searchLinks( + @Query("page") page: Int = 0, + @Query("size") size: Int = 10, + @Query("sort") sort: List = listOf(LinksSort.RECENT.value), + @Query("isRead") isRead: Boolean = false, + @Query("favorites") favorites: Boolean = false, + @Query("startDate") startDate: String? = null, + @Query("endDate") endDate: String? = null, + @Query("categoryIds") categoryIds: List? = null, + @Query("searchWord") searchWord: String = "", + ): GetLinksResponse + + @PUT("content/{contentId}") + suspend fun deleteLink( + @Path("contentId") contentId: Int = 0, + ) + + @POST("content/{contentId}") + suspend fun getLink( + @Path("contentId") contentId: Int = 0, + ): GetLinkResponse + + @PATCH("content/{contentId}") + suspend fun modifyLink( + @Path("contentId") contentId: Int, + @Body modifyLinkRequest: ModifyLinkRequest, + ): ModifyLinkResponse + + @POST("content") + suspend fun createLink( + @Body createLinkRequest: ModifyLinkRequest, + ): ModifyLinkResponse + + @PUT("content/{contentId}/bookmark") + suspend fun cancelBookmark(@Path("contentId") contentId: Int) + + @POST("content/{contentId}/bookmark") + suspend fun applyBookmark(@Path("contentId") contentId: Int): ApplyBookmarkResponse + + @GET("content/uncategorized") + suspend fun getUncategorizedLinks( + @Query("page") page: Int = 0, + @Query("size") size: Int = 10, + @Query("sort") sort: List = listOf(LinksSort.RECENT.value), + ): GetLinksResponse +} diff --git a/data/src/main/java/pokitmons/pokit/data/api/PokitApi.kt b/data/src/main/java/pokitmons/pokit/data/api/PokitApi.kt new file mode 100644 index 00000000..c271b0fb --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/api/PokitApi.kt @@ -0,0 +1,55 @@ +package pokitmons.pokit.data.api + +import pokitmons.pokit.data.model.pokit.request.CreatePokitRequest +import pokitmons.pokit.data.model.pokit.request.ModifyPokitRequest +import pokitmons.pokit.data.model.pokit.response.CreatePokitResponse +import pokitmons.pokit.data.model.pokit.response.GetPokitCountResponse +import pokitmons.pokit.data.model.pokit.response.GetPokitImagesResponseItem +import pokitmons.pokit.data.model.pokit.response.GetPokitResponse +import pokitmons.pokit.data.model.pokit.response.GetPokitsResponse +import pokitmons.pokit.data.model.pokit.response.ModifyPokitResponse +import pokitmons.pokit.domain.model.pokit.PokitsSort +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.PATCH +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Path +import retrofit2.http.Query + +interface PokitApi { + @GET("category") + suspend fun getPokits( + @Query("filterUncategorized") filterUncategorized: Boolean = true, + @Query("size") size: Int = 10, + @Query("page") page: Int = 0, + @Query("sort") sort: String = PokitsSort.RECENT.value, + ): GetPokitsResponse + + @POST("category") + suspend fun createPokit( + @Body createPokitRequest: CreatePokitRequest, + ): CreatePokitResponse + + @PATCH("category/{categoryId}") + suspend fun modifyPokit( + @Path("categoryId") categoryId: Int, + @Body modifyPokitRequest: ModifyPokitRequest, + ): ModifyPokitResponse + + @GET("category/images") + suspend fun getPokitImages(): List + + @GET("category/{categoryId}") + suspend fun getPokit( + @Path("categoryId") categoryId: Int, + ): GetPokitResponse + + @PUT("category/{categoryId}") + suspend fun deletePokit( + @Path("categoryId") categoryId: Int, + ) + + @GET("category/count") + suspend fun getPokitCount(): GetPokitCountResponse +} diff --git a/data/src/main/java/pokitmons/pokit/data/api/RemindApi.kt b/data/src/main/java/pokitmons/pokit/data/api/RemindApi.kt new file mode 100644 index 00000000..b912b7f8 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/api/RemindApi.kt @@ -0,0 +1,30 @@ +package pokitmons.pokit.data.api + +import pokitmons.pokit.data.model.home.remind.Remind +import pokitmons.pokit.data.model.home.remind.RemindResponse +import pokitmons.pokit.domain.model.pokit.PokitsSort +import retrofit2.http.GET +import retrofit2.http.Query + +interface RemindApi { + @GET("remind/unread") + suspend fun getUnreadContents( + @Query("size") size: Int = 10, + @Query("page") page: Int = 0, + @Query("sort") sort: String = PokitsSort.RECENT.value, + ): RemindResponse + + @GET("remind/today") + suspend fun getTodayContents( + @Query("size") size: Int = 10, + @Query("page") page: Int = 0, + @Query("sort") sort: String = PokitsSort.RECENT.value, + ): List + + @GET("remind/bookmark") + suspend fun getBookmarkContents( + @Query("size") size: Int = 10, + @Query("page") page: Int = 0, + @Query("sort") sort: String = PokitsSort.RECENT.value, + ): RemindResponse +} diff --git a/data/src/main/java/pokitmons/pokit/data/api/SettingApi.kt b/data/src/main/java/pokitmons/pokit/data/api/SettingApi.kt new file mode 100644 index 00000000..a9140129 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/api/SettingApi.kt @@ -0,0 +1,13 @@ +package pokitmons.pokit.data.api + +import pokitmons.pokit.data.model.setting.reqeust.EditNicknameRequest +import pokitmons.pokit.data.model.setting.response.EditNicknameResponse +import retrofit2.http.Body +import retrofit2.http.PUT + +interface SettingApi { + @PUT("user/nickname") + suspend fun editNickname( + @Body editNicknameRequest: EditNicknameRequest, + ): EditNicknameResponse +} diff --git a/data/src/main/java/pokitmons/pokit/data/datasource/local/search/LocalSearchWordDataSource.kt b/data/src/main/java/pokitmons/pokit/data/datasource/local/search/LocalSearchWordDataSource.kt new file mode 100644 index 00000000..34b39370 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/datasource/local/search/LocalSearchWordDataSource.kt @@ -0,0 +1,54 @@ +package pokitmons.pokit.data.datasource.local.search + +import android.content.SharedPreferences +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import pokitmons.pokit.data.room.dao.SearchWordDao +import pokitmons.pokit.data.room.entity.SearchWord +import java.util.Calendar +import javax.inject.Inject + +class LocalSearchWordDataSource @Inject constructor( + private val searchWordDao: SearchWordDao, + private val sharedPreferences: SharedPreferences, +) : SearchDataSource { + companion object { + const val USE_RECENT_WORD_SP_KEY = "use_recent_word" + } + + private val useRecentSearchWords = MutableStateFlow( + sharedPreferences.getBoolean(USE_RECENT_WORD_SP_KEY, false) + ) + + override fun getSearchWord(): Flow> { + return searchWordDao.getRecentSearchWords() + } + + override suspend fun addSearchWord(searchWord: String) { + val currentDateString = Calendar.getInstance() + val searchWordEntity = SearchWord( + word = searchWord, + searchedAt = currentDateString.timeInMillis.toString() + ) + searchWordDao.addSearchWord(searchWord = searchWordEntity) + } + + override suspend fun removeSearchWord(searchWord: String) { + searchWordDao.removeSearchWord(searchWord) + } + + override suspend fun removeAllSearchWords() { + searchWordDao.removeAllSearchWords() + } + + override suspend fun setUseRecentSearchWord(use: Boolean): Boolean { + sharedPreferences.edit().putBoolean(USE_RECENT_WORD_SP_KEY, use).apply() + useRecentSearchWords.update { use } + return use + } + + override fun getUseRecentSearchWord(): Flow { + return useRecentSearchWords + } +} diff --git a/data/src/main/java/pokitmons/pokit/data/datasource/local/search/SearchDataSource.kt b/data/src/main/java/pokitmons/pokit/data/datasource/local/search/SearchDataSource.kt new file mode 100644 index 00000000..77e8f99c --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/datasource/local/search/SearchDataSource.kt @@ -0,0 +1,12 @@ +package pokitmons.pokit.data.datasource.local.search + +import kotlinx.coroutines.flow.Flow + +interface SearchDataSource { + fun getSearchWord(): Flow> + suspend fun addSearchWord(searchWord: String) + suspend fun removeSearchWord(searchWord: String) + suspend fun removeAllSearchWords() + suspend fun setUseRecentSearchWord(use: Boolean): Boolean + fun getUseRecentSearchWord(): Flow +} diff --git a/data/src/main/java/pokitmons/pokit/data/datasource/remote/alert/AlertDataSource.kt b/data/src/main/java/pokitmons/pokit/data/datasource/remote/alert/AlertDataSource.kt new file mode 100644 index 00000000..8cbaeeec --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/datasource/remote/alert/AlertDataSource.kt @@ -0,0 +1,8 @@ +package pokitmons.pokit.data.datasource.remote.alert + +import pokitmons.pokit.data.model.alert.GetAlertsResponse + +interface AlertDataSource { + suspend fun getAlerts(page: Int, size: Int): GetAlertsResponse + suspend fun deleteAlert(alertId: Int) +} diff --git a/data/src/main/java/pokitmons/pokit/data/datasource/remote/alert/RemoteAlertDataSource.kt b/data/src/main/java/pokitmons/pokit/data/datasource/remote/alert/RemoteAlertDataSource.kt new file mode 100644 index 00000000..d9f3cf8f --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/datasource/remote/alert/RemoteAlertDataSource.kt @@ -0,0 +1,17 @@ +package pokitmons.pokit.data.datasource.remote.alert + +import pokitmons.pokit.data.api.AlertApi +import pokitmons.pokit.data.model.alert.GetAlertsResponse +import javax.inject.Inject + +class RemoteAlertDataSource @Inject constructor( + private val api: AlertApi, +) : AlertDataSource { + override suspend fun getAlerts(page: Int, size: Int): GetAlertsResponse { + return api.getAlerts(page = page, size = size) + } + + override suspend fun deleteAlert(alertId: Int) { + return api.deleteAlert(alertId) + } +} diff --git a/data/src/main/java/pokitmons/pokit/data/datasource/remote/auth/AuthDataSource.kt b/data/src/main/java/pokitmons/pokit/data/datasource/remote/auth/AuthDataSource.kt index 28f411c4..5ab10086 100644 --- a/data/src/main/java/pokitmons/pokit/data/datasource/remote/auth/AuthDataSource.kt +++ b/data/src/main/java/pokitmons/pokit/data/datasource/remote/auth/AuthDataSource.kt @@ -1,11 +1,13 @@ package pokitmons.pokit.data.datasource.remote.auth import pokitmons.pokit.data.model.auth.request.SNSLoginRequest +import pokitmons.pokit.data.model.auth.request.SignUpRequest import pokitmons.pokit.data.model.auth.response.DuplicateNicknameResponse import pokitmons.pokit.data.model.auth.response.SNSLoginResponse +import pokitmons.pokit.data.model.auth.response.SignUpResponse interface AuthDataSource { -// suspend fun signUp(signUpRequest: SignUpRequest): PokitResponse + suspend fun signUp(signUpRequest: SignUpRequest): SignUpResponse suspend fun snsLogin(snsLoginRequest: SNSLoginRequest): SNSLoginResponse suspend fun checkDuplicateNickname(nickname: String): DuplicateNicknameResponse } diff --git a/data/src/main/java/pokitmons/pokit/data/datasource/remote/auth/RemoteAuthDataSourceImpl.kt b/data/src/main/java/pokitmons/pokit/data/datasource/remote/auth/RemoteAuthDataSourceImpl.kt index 7b1c521e..02f030e4 100644 --- a/data/src/main/java/pokitmons/pokit/data/datasource/remote/auth/RemoteAuthDataSourceImpl.kt +++ b/data/src/main/java/pokitmons/pokit/data/datasource/remote/auth/RemoteAuthDataSourceImpl.kt @@ -2,8 +2,10 @@ package pokitmons.pokit.data.datasource.remote.auth import pokitmons.pokit.data.api.AuthApi import pokitmons.pokit.data.model.auth.request.SNSLoginRequest +import pokitmons.pokit.data.model.auth.request.SignUpRequest import pokitmons.pokit.data.model.auth.response.DuplicateNicknameResponse import pokitmons.pokit.data.model.auth.response.SNSLoginResponse +import pokitmons.pokit.data.model.auth.response.SignUpResponse import javax.inject.Inject class RemoteAuthDataSourceImpl @Inject constructor(private val authApi: AuthApi) : AuthDataSource { @@ -14,4 +16,8 @@ class RemoteAuthDataSourceImpl @Inject constructor(private val authApi: AuthApi) override suspend fun checkDuplicateNickname(nickname: String): DuplicateNicknameResponse { return authApi.checkDuplicateNickname(nickname) } + + override suspend fun signUp(signUpRequest: SignUpRequest): SignUpResponse { + return authApi.signUp(signUpRequest) + } } diff --git a/data/src/main/java/pokitmons/pokit/data/datasource/remote/home/remind/RemindDataSource.kt b/data/src/main/java/pokitmons/pokit/data/datasource/remote/home/remind/RemindDataSource.kt new file mode 100644 index 00000000..dbf1ee25 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/datasource/remote/home/remind/RemindDataSource.kt @@ -0,0 +1,11 @@ +package pokitmons.pokit.data.datasource.remote.home.remind + +import pokitmons.pokit.data.model.home.remind.Remind +import pokitmons.pokit.data.model.home.remind.RemindRequest +import pokitmons.pokit.data.model.home.remind.RemindResponse + +interface RemindDataSource { + suspend fun getUnreadContents(remindRequest: RemindRequest): RemindResponse + suspend fun getTodayContents(remindRequest: RemindRequest): List + suspend fun getBookmarkContents(remindRequest: RemindRequest): RemindResponse +} diff --git a/data/src/main/java/pokitmons/pokit/data/datasource/remote/home/remind/RemindDataSourceImpl.kt b/data/src/main/java/pokitmons/pokit/data/datasource/remote/home/remind/RemindDataSourceImpl.kt new file mode 100644 index 00000000..0b88996f --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/datasource/remote/home/remind/RemindDataSourceImpl.kt @@ -0,0 +1,21 @@ +package pokitmons.pokit.data.datasource.remote.home.remind + +import pokitmons.pokit.data.api.RemindApi +import pokitmons.pokit.data.model.home.remind.Remind +import pokitmons.pokit.data.model.home.remind.RemindRequest +import pokitmons.pokit.data.model.home.remind.RemindResponse +import javax.inject.Inject + +class RemindDataSourceImpl @Inject constructor(private val remindApi: RemindApi) : RemindDataSource { + override suspend fun getUnreadContents(remindRequest: RemindRequest): RemindResponse { + return remindApi.getUnreadContents() + } + + override suspend fun getTodayContents(remindRequest: RemindRequest): List { + return remindApi.getTodayContents() + } + + override suspend fun getBookmarkContents(remindRequest: RemindRequest): RemindResponse { + return remindApi.getBookmarkContents() + } +} diff --git a/data/src/main/java/pokitmons/pokit/data/datasource/remote/link/LinkDataSource.kt b/data/src/main/java/pokitmons/pokit/data/datasource/remote/link/LinkDataSource.kt new file mode 100644 index 00000000..3bca87ee --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/datasource/remote/link/LinkDataSource.kt @@ -0,0 +1,57 @@ +package pokitmons.pokit.data.datasource.remote.link + +import pokitmons.pokit.data.model.link.request.ModifyLinkRequest +import pokitmons.pokit.data.model.link.response.GetLinkResponse +import pokitmons.pokit.data.model.link.response.GetLinksResponse +import pokitmons.pokit.data.model.link.response.LinkCardResponse +import pokitmons.pokit.data.model.link.response.ModifyLinkResponse +import pokitmons.pokit.domain.model.link.LinksSort + +interface LinkDataSource { + suspend fun getLinks( + categoryId: Int = 0, + page: Int = 0, + size: Int = 10, + sort: List = listOf(LinksSort.RECENT.value), + isRead: Boolean = false, + favorites: Boolean = false, + startDate: String? = null, + endDate: String? = null, + categoryIds: List? = null, + ): GetLinksResponse + + suspend fun searchLinks( + page: Int = 0, + size: Int = 10, + sort: List = listOf(LinksSort.RECENT.value), + isRead: Boolean = false, + favorites: Boolean = false, + startDate: String? = null, + endDate: String? = null, + categoryIds: List? = null, + searchWord: String = "", + ): GetLinksResponse + + suspend fun deleteLink(contentId: Int) + + suspend fun getLink(contentId: Int): GetLinkResponse + + suspend fun modifyLink( + contentId: Int, + modifyLinkRequest: ModifyLinkRequest, + ): ModifyLinkResponse + + suspend fun createLink( + createLinkRequest: ModifyLinkRequest, + ): ModifyLinkResponse + + suspend fun setBookmark(contentId: Int, bookmarked: Boolean) + + suspend fun getLinkCard(url: String): LinkCardResponse + + suspend fun getUncategorizedLinks( + page: Int = 0, + size: Int = 10, + sort: List = listOf(LinksSort.RECENT.value), + ): GetLinksResponse +} diff --git a/data/src/main/java/pokitmons/pokit/data/datasource/remote/link/RemoteLinkDataSource.kt b/data/src/main/java/pokitmons/pokit/data/datasource/remote/link/RemoteLinkDataSource.kt new file mode 100644 index 00000000..a0b965ef --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/datasource/remote/link/RemoteLinkDataSource.kt @@ -0,0 +1,109 @@ +package pokitmons.pokit.data.datasource.remote.link + +import org.jsoup.Jsoup +import pokitmons.pokit.data.api.LinkApi +import pokitmons.pokit.data.model.link.request.ModifyLinkRequest +import pokitmons.pokit.data.model.link.response.GetLinkResponse +import pokitmons.pokit.data.model.link.response.GetLinksResponse +import pokitmons.pokit.data.model.link.response.LinkCardResponse +import pokitmons.pokit.data.model.link.response.ModifyLinkResponse +import javax.inject.Inject + +class RemoteLinkDataSource @Inject constructor( + private val linkApi: LinkApi, +) : LinkDataSource { + override suspend fun getLinks( + categoryId: Int, + page: Int, + size: Int, + sort: List, + isRead: Boolean, + favorites: Boolean, + startDate: String?, + endDate: String?, + categoryIds: List?, + ): GetLinksResponse { + return linkApi.getLinks( + categoryId = categoryId, + page = page, + size = size, + sort = sort, + isRead = isRead, + favorites = favorites, + startDate = startDate, + endDate = endDate, + categoryIds = categoryIds + ) + } + + override suspend fun searchLinks( + page: Int, + size: Int, + sort: List, + isRead: Boolean, + favorites: Boolean, + startDate: String?, + endDate: String?, + categoryIds: List?, + searchWord: String, + ): GetLinksResponse { + return linkApi.searchLinks( + page = page, + size = size, + sort = sort, + isRead = isRead, + favorites = favorites, + startDate = startDate, + endDate = endDate, + categoryIds = categoryIds, + searchWord = searchWord + ) + } + + override suspend fun deleteLink(contentId: Int) { + return linkApi.deleteLink(contentId = contentId) + } + + override suspend fun getLink(contentId: Int): GetLinkResponse { + return linkApi.getLink(contentId) + } + + override suspend fun modifyLink( + contentId: Int, + modifyLinkRequest: ModifyLinkRequest, + ): ModifyLinkResponse { + return linkApi.modifyLink( + contentId = contentId, + modifyLinkRequest = modifyLinkRequest + ) + } + + override suspend fun createLink(createLinkRequest: ModifyLinkRequest): ModifyLinkResponse { + return linkApi.createLink( + createLinkRequest = createLinkRequest + ) + } + + override suspend fun setBookmark(contentId: Int, bookmarked: Boolean) { + if (bookmarked) { + linkApi.applyBookmark(contentId) + } else { + linkApi.cancelBookmark(contentId) + } + } + + override suspend fun getLinkCard(url: String): LinkCardResponse { + val document = Jsoup.connect(url).get() + val image = document.select("meta[property=og:image]").attr("content").ifEmpty { null } + val title = document.select("meta[property=og:title]").attr("content") + return LinkCardResponse( + url = url, + image = image, + title = title + ) + } + + override suspend fun getUncategorizedLinks(page: Int, size: Int, sort: List): GetLinksResponse { + return linkApi.getUncategorizedLinks(page = page, size = size, sort = sort) + } +} diff --git a/data/src/main/java/pokitmons/pokit/data/datasource/remote/pokit/PokitDataSource.kt b/data/src/main/java/pokitmons/pokit/data/datasource/remote/pokit/PokitDataSource.kt new file mode 100644 index 00000000..1032b7e8 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/datasource/remote/pokit/PokitDataSource.kt @@ -0,0 +1,21 @@ +package pokitmons.pokit.data.datasource.remote.pokit + +import pokitmons.pokit.data.model.pokit.request.CreatePokitRequest +import pokitmons.pokit.data.model.pokit.request.GetPokitsRequest +import pokitmons.pokit.data.model.pokit.request.ModifyPokitRequest +import pokitmons.pokit.data.model.pokit.response.CreatePokitResponse +import pokitmons.pokit.data.model.pokit.response.GetPokitCountResponse +import pokitmons.pokit.data.model.pokit.response.GetPokitImagesResponseItem +import pokitmons.pokit.data.model.pokit.response.GetPokitResponse +import pokitmons.pokit.data.model.pokit.response.GetPokitsResponse +import pokitmons.pokit.data.model.pokit.response.ModifyPokitResponse + +interface PokitDataSource { + suspend fun getPokits(getPokitsRequest: GetPokitsRequest): GetPokitsResponse + suspend fun createPokit(createPokitRequest: CreatePokitRequest): CreatePokitResponse + suspend fun modifyPokit(pokitId: Int, modifyPokitRequest: ModifyPokitRequest): ModifyPokitResponse + suspend fun getPokitImages(): List + suspend fun getPokit(pokitId: Int): GetPokitResponse + suspend fun deletePokit(pokitId: Int) + suspend fun getPokitCount(): GetPokitCountResponse +} diff --git a/data/src/main/java/pokitmons/pokit/data/datasource/remote/pokit/RemotePokitDataSource.kt b/data/src/main/java/pokitmons/pokit/data/datasource/remote/pokit/RemotePokitDataSource.kt new file mode 100644 index 00000000..1d1614c8 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/datasource/remote/pokit/RemotePokitDataSource.kt @@ -0,0 +1,50 @@ +package pokitmons.pokit.data.datasource.remote.pokit + +import pokitmons.pokit.data.api.PokitApi +import pokitmons.pokit.data.model.pokit.request.CreatePokitRequest +import pokitmons.pokit.data.model.pokit.request.GetPokitsRequest +import pokitmons.pokit.data.model.pokit.request.ModifyPokitRequest +import pokitmons.pokit.data.model.pokit.response.CreatePokitResponse +import pokitmons.pokit.data.model.pokit.response.GetPokitCountResponse +import pokitmons.pokit.data.model.pokit.response.GetPokitImagesResponseItem +import pokitmons.pokit.data.model.pokit.response.GetPokitResponse +import pokitmons.pokit.data.model.pokit.response.GetPokitsResponse +import pokitmons.pokit.data.model.pokit.response.ModifyPokitResponse +import javax.inject.Inject + +class RemotePokitDataSource @Inject constructor( + private val pokitApi: PokitApi, +) : PokitDataSource { + override suspend fun getPokits(getPokitsRequest: GetPokitsRequest): GetPokitsResponse { + return pokitApi.getPokits( + filterUncategorized = getPokitsRequest.filterUncategoriezd, + size = getPokitsRequest.size, + page = getPokitsRequest.page, + sort = getPokitsRequest.sort.value + ) + } + + override suspend fun createPokit(createPokitRequest: CreatePokitRequest): CreatePokitResponse { + return pokitApi.createPokit(createPokitRequest = createPokitRequest) + } + + override suspend fun modifyPokit(pokitId: Int, modifyPokitRequest: ModifyPokitRequest): ModifyPokitResponse { + return pokitApi.modifyPokit(categoryId = pokitId, modifyPokitRequest = modifyPokitRequest) + } + + override suspend fun getPokitImages(): List { + return pokitApi.getPokitImages() + } + + override suspend fun getPokit(pokitId: Int): GetPokitResponse { + return pokitApi.getPokit(pokitId) + } + + override suspend fun deletePokit(pokitId: Int) { + return pokitApi.deletePokit(pokitId) + } + + override suspend fun getPokitCount(): GetPokitCountResponse { + return pokitApi.getPokitCount() + } +} diff --git a/data/src/main/java/pokitmons/pokit/data/datasource/remote/setting/RemoteSettingDataSourceImpl.kt b/data/src/main/java/pokitmons/pokit/data/datasource/remote/setting/RemoteSettingDataSourceImpl.kt new file mode 100644 index 00000000..604dbb52 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/datasource/remote/setting/RemoteSettingDataSourceImpl.kt @@ -0,0 +1,12 @@ +package pokitmons.pokit.data.datasource.remote.setting + +import pokitmons.pokit.data.api.SettingApi +import pokitmons.pokit.data.model.setting.reqeust.EditNicknameRequest +import pokitmons.pokit.data.model.setting.response.EditNicknameResponse +import javax.inject.Inject + +class RemoteSettingDataSourceImpl @Inject constructor(private val settingApi: SettingApi) : SettingDataSource { + override suspend fun editNickname(editNicknameRequest: EditNicknameRequest): EditNicknameResponse { + return settingApi.editNickname(editNicknameRequest) + } +} diff --git a/data/src/main/java/pokitmons/pokit/data/datasource/remote/setting/SettingDataSource.kt b/data/src/main/java/pokitmons/pokit/data/datasource/remote/setting/SettingDataSource.kt new file mode 100644 index 00000000..f9edb38e --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/datasource/remote/setting/SettingDataSource.kt @@ -0,0 +1,8 @@ +package pokitmons.pokit.data.datasource.remote.setting + +import pokitmons.pokit.data.model.setting.reqeust.EditNicknameRequest +import pokitmons.pokit.data.model.setting.response.EditNicknameResponse + +interface SettingDataSource { + suspend fun editNickname(editNicknameRequest: EditNicknameRequest): EditNicknameResponse +} diff --git a/data/src/main/java/pokitmons/pokit/data/di/alert/AlertModule.kt b/data/src/main/java/pokitmons/pokit/data/di/alert/AlertModule.kt new file mode 100644 index 00000000..748138e7 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/di/alert/AlertModule.kt @@ -0,0 +1,23 @@ +package pokitmons.pokit.data.di.alert + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import pokitmons.pokit.data.datasource.remote.alert.AlertDataSource +import pokitmons.pokit.data.datasource.remote.alert.RemoteAlertDataSource +import pokitmons.pokit.data.repository.alert.AlertRepositoryImpl +import pokitmons.pokit.domain.repository.alert.AlertRepository +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class AlertModule { + @Binds + @Singleton + abstract fun bindAlertRepository(alertRepositoryImpl: AlertRepositoryImpl): AlertRepository + + @Binds + @Singleton + abstract fun bindAlertDataSource(alertDataSourceImpl: RemoteAlertDataSource): AlertDataSource +} diff --git a/data/src/main/java/pokitmons/pokit/data/di/core/room/RoomModule.kt b/data/src/main/java/pokitmons/pokit/data/di/core/room/RoomModule.kt new file mode 100644 index 00000000..cc7126ee --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/di/core/room/RoomModule.kt @@ -0,0 +1,25 @@ +package pokitmons.pokit.data.di.core.room + +import android.content.Context +import androidx.room.Room +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import pokitmons.pokit.data.room.database.AppDatabase +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object RoomModule { + @Provides + @Singleton + fun provideDatabase(@ApplicationContext context: Context): AppDatabase { + return Room.databaseBuilder(context, AppDatabase::class.java, "pokitDatabase.db").build() + } + + @Provides + @Singleton + fun providerSearchWordDao(database: AppDatabase) = database.searchWordDao() +} diff --git a/data/src/main/java/pokitmons/pokit/data/di/core/sharedpreferences/SharedPreferencesModule.kt b/data/src/main/java/pokitmons/pokit/data/di/core/sharedpreferences/SharedPreferencesModule.kt new file mode 100644 index 00000000..7d6c6419 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/di/core/sharedpreferences/SharedPreferencesModule.kt @@ -0,0 +1,23 @@ +package pokitmons.pokit.data.di.core.sharedpreferences + +import android.content.Context +import android.content.Context.MODE_PRIVATE +import android.content.SharedPreferences +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object SharedPreferencesModule { + @Provides + @Singleton + fun provideSharedPreferences( + @ApplicationContext context: Context, + ): SharedPreferences { + return context.getSharedPreferences("pokit_shared_preferences", MODE_PRIVATE) + } +} diff --git a/data/src/main/java/pokitmons/pokit/data/di/home/remind/RemindModule.kt b/data/src/main/java/pokitmons/pokit/data/di/home/remind/RemindModule.kt new file mode 100644 index 00000000..3905e080 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/di/home/remind/RemindModule.kt @@ -0,0 +1,23 @@ +package pokitmons.pokit.data.di.home.remind + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import pokitmons.pokit.data.datasource.remote.home.remind.RemindDataSource +import pokitmons.pokit.data.datasource.remote.home.remind.RemindDataSourceImpl +import pokitmons.pokit.data.repository.home.remind.RemindRepositoryImpl +import pokitmons.pokit.domain.repository.home.remind.RemindRepository +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class RemindModule { + @Binds + @Singleton + abstract fun bindRemindRepository(remindRepositoryImpl: RemindRepositoryImpl): RemindRepository + + @Binds + @Singleton + abstract fun bindRemindDataSource(remindDataSourceImpl: RemindDataSourceImpl): RemindDataSource +} diff --git a/data/src/main/java/pokitmons/pokit/data/di/link/LinkModule.kt b/data/src/main/java/pokitmons/pokit/data/di/link/LinkModule.kt new file mode 100644 index 00000000..61eadde8 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/di/link/LinkModule.kt @@ -0,0 +1,23 @@ +package pokitmons.pokit.data.di.link + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import pokitmons.pokit.data.datasource.remote.link.LinkDataSource +import pokitmons.pokit.data.datasource.remote.link.RemoteLinkDataSource +import pokitmons.pokit.data.repository.link.LinkRepositoryImpl +import pokitmons.pokit.domain.repository.link.LinkRepository +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class LinkModule { + @Binds + @Singleton + abstract fun bindLinkRepository(linkRepositoryImpl: LinkRepositoryImpl): LinkRepository + + @Binds + @Singleton + abstract fun bindLinkDataSource(linkDataSourceImpl: RemoteLinkDataSource): LinkDataSource +} diff --git a/data/src/main/java/pokitmons/pokit/data/di/network/BearerTokenInterceptor.kt b/data/src/main/java/pokitmons/pokit/data/di/network/BearerTokenInterceptor.kt index 769fc857..1a5fe4ed 100644 --- a/data/src/main/java/pokitmons/pokit/data/di/network/BearerTokenInterceptor.kt +++ b/data/src/main/java/pokitmons/pokit/data/di/network/BearerTokenInterceptor.kt @@ -13,8 +13,8 @@ class BearerTokenInterceptor : Interceptor { val requestWithToken: Request = originalRequest.newBuilder() .header( "Authorization", - "Bearer eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiIxIiwiaWF0IjoxNzIxNjU4MjUxLCJleHAiOj" + - "MwMDE3MjE2NTgyNTF9.gw6LZimKLuZJ2y0UV5cgvk3F7o92pkRIDgx-qlD_S7qEI01QAFt9dZDyHADabftI" + "Bearer eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiIxMCIsImlhdCI6MTcyMzY0MzEzOSwiZXhwIjoy" + + "MDIzNjQzMTM5fQ.3jJ6rpPCaMKSrmiB3NtQ3_sYH0zbBuoS0GAwX69HCu62-Vk6x--eUu4dhZJTmqlm" ) .build() diff --git a/data/src/main/java/pokitmons/pokit/data/di/network/NetworkModule.kt b/data/src/main/java/pokitmons/pokit/data/di/network/NetworkModule.kt index 5d5371cf..05389780 100644 --- a/data/src/main/java/pokitmons/pokit/data/di/network/NetworkModule.kt +++ b/data/src/main/java/pokitmons/pokit/data/di/network/NetworkModule.kt @@ -9,7 +9,12 @@ import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor +import pokitmons.pokit.data.api.AlertApi import pokitmons.pokit.data.api.AuthApi +import pokitmons.pokit.data.api.LinkApi +import pokitmons.pokit.data.api.PokitApi +import pokitmons.pokit.data.api.RemindApi +import pokitmons.pokit.data.api.SettingApi import retrofit2.Retrofit import java.util.concurrent.TimeUnit import javax.inject.Singleton @@ -67,4 +72,29 @@ object NetworkModule { fun provideAuthService(retrofit: Retrofit): AuthApi { return retrofit.create(AuthApi::class.java) } + + @Provides + fun providePokitService(retrofit: Retrofit): PokitApi { + return retrofit.create(PokitApi::class.java) + } + + @Provides + fun provideLinkService(retrofit: Retrofit): LinkApi { + return retrofit.create(LinkApi::class.java) + } + + @Provides + fun provideSettingService(retrofit: Retrofit): SettingApi { + return retrofit.create(SettingApi::class.java) + } + + @Provides + fun provideRemindService(retrofit: Retrofit): RemindApi { + return retrofit.create(RemindApi::class.java) + } + + @Provides + fun provideAlertService(retrofit: Retrofit): AlertApi { + return retrofit.create(AlertApi::class.java) + } } diff --git a/data/src/main/java/pokitmons/pokit/data/di/pokit/PokitModule.kt b/data/src/main/java/pokitmons/pokit/data/di/pokit/PokitModule.kt new file mode 100644 index 00000000..32c1014a --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/di/pokit/PokitModule.kt @@ -0,0 +1,23 @@ +package pokitmons.pokit.data.di.pokit + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import pokitmons.pokit.data.datasource.remote.pokit.PokitDataSource +import pokitmons.pokit.data.datasource.remote.pokit.RemotePokitDataSource +import pokitmons.pokit.data.repository.pokit.PokitRepositoryImpl +import pokitmons.pokit.domain.repository.pokit.PokitRepository +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class PokitModule { + @Binds + @Singleton + abstract fun bindPokitRepository(pokitRepositoryImpl: PokitRepositoryImpl): PokitRepository + + @Binds + @Singleton + abstract fun bindPokitDataSource(pokitDataSourceImpl: RemotePokitDataSource): PokitDataSource +} diff --git a/data/src/main/java/pokitmons/pokit/data/di/search/SearchModule.kt b/data/src/main/java/pokitmons/pokit/data/di/search/SearchModule.kt new file mode 100644 index 00000000..dbd7a4a4 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/di/search/SearchModule.kt @@ -0,0 +1,23 @@ +package pokitmons.pokit.data.di.search + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import pokitmons.pokit.data.datasource.local.search.LocalSearchWordDataSource +import pokitmons.pokit.data.datasource.local.search.SearchDataSource +import pokitmons.pokit.data.repository.search.SearchRepositoryImpl +import pokitmons.pokit.domain.repository.search.SearchRepository +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class SearchModule { + @Binds + @Singleton + abstract fun bindSearchRepository(searchRepositoryImpl: SearchRepositoryImpl): SearchRepository + + @Binds + @Singleton + abstract fun bindSearchDataSource(searchDataSourceImpl: LocalSearchWordDataSource): SearchDataSource +} diff --git a/data/src/main/java/pokitmons/pokit/data/di/setting/SettingModule.kt b/data/src/main/java/pokitmons/pokit/data/di/setting/SettingModule.kt new file mode 100644 index 00000000..ec050258 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/di/setting/SettingModule.kt @@ -0,0 +1,23 @@ +package pokitmons.pokit.data.di.setting + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import pokitmons.pokit.data.datasource.remote.setting.RemoteSettingDataSourceImpl +import pokitmons.pokit.data.datasource.remote.setting.SettingDataSource +import pokitmons.pokit.data.repository.setting.SettingRepositoryImpl +import pokitmons.pokit.domain.repository.setting.SettingRepository +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class SettingModule { + @Binds + @Singleton + abstract fun bindSettingRepository(settingRepositoryImpl: SettingRepositoryImpl): SettingRepository + + @Binds + @Singleton + abstract fun bindSettingDataSource(settingDataSourceImpl: RemoteSettingDataSourceImpl): SettingDataSource +} diff --git a/data/src/main/java/pokitmons/pokit/data/mapper/alert/AlertMapper.kt b/data/src/main/java/pokitmons/pokit/data/mapper/alert/AlertMapper.kt new file mode 100644 index 00000000..196af500 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/mapper/alert/AlertMapper.kt @@ -0,0 +1,18 @@ +package pokitmons.pokit.data.mapper.alert + +import pokitmons.pokit.data.model.alert.GetAlertsResponse +import pokitmons.pokit.domain.model.alert.Alarm + +object AlertMapper { + fun mapperToAlarmList(response: GetAlertsResponse): List { + return response.data.map { data -> + Alarm( + id = data.id, + contentId = data.contentId, + thumbnail = data.thumbNail, + title = data.title, + createdAt = data.createdAt + ) + } + } +} diff --git a/data/src/main/java/pokitmons/pokit/data/mapper/auth/AuthMapper.kt b/data/src/main/java/pokitmons/pokit/data/mapper/auth/AuthMapper.kt index 6168b2d5..9a528c28 100644 --- a/data/src/main/java/pokitmons/pokit/data/mapper/auth/AuthMapper.kt +++ b/data/src/main/java/pokitmons/pokit/data/mapper/auth/AuthMapper.kt @@ -2,8 +2,10 @@ package pokitmons.pokit.data.mapper.auth import pokitmons.pokit.data.model.auth.response.DuplicateNicknameResponse import pokitmons.pokit.data.model.auth.response.SNSLoginResponse +import pokitmons.pokit.data.model.auth.response.SignUpResponse import pokitmons.pokit.domain.model.auth.DuplicateNicknameResult import pokitmons.pokit.domain.model.auth.SNSLoginResult +import pokitmons.pokit.domain.model.auth.SignUpResult object AuthMapper { fun mapperToSNSLogin(snsLoginResponse: SNSLoginResponse): SNSLoginResult { @@ -18,4 +20,12 @@ object AuthMapper { isDuplicate = checkDuplicateNicknameResponse.isDuplicate ) } + + fun mapperToSignUp(signUpResponse: SignUpResponse): SignUpResult { + return SignUpResult( + id = signUpResponse.id, + email = signUpResponse.email, + nickname = signUpResponse.nickname + ) + } } diff --git a/data/src/main/java/pokitmons/pokit/data/mapper/home/home/RemindMapper.kt b/data/src/main/java/pokitmons/pokit/data/mapper/home/home/RemindMapper.kt new file mode 100644 index 00000000..f95a100d --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/mapper/home/home/RemindMapper.kt @@ -0,0 +1,33 @@ +package pokitmons.pokit.data.mapper.home.home + +import pokitmons.pokit.data.model.home.remind.Remind +import pokitmons.pokit.data.model.home.remind.RemindResponse +import pokitmons.pokit.domain.model.home.remind.RemindResult + +object RemindMapper { + fun mapperToRemind(remindResponse: RemindResponse): List { + return remindResponse.data.map { remind -> + RemindResult( + title = remind.title, + domain = remind.domain, + createdAt = remind.createdAt, + isRead = remind.isRead, + thumbNail = remind.thumbNail, + data = remind.data + ) + } + } + + fun mapperToTodayContents(remindResponse: List): List { + return remindResponse.map { remind -> + RemindResult( + title = remind.title, + domain = remind.domain, + createdAt = remind.createdAt, + isRead = remind.isRead, + thumbNail = remind.thumbNail, + data = remind.data + ) + } + } +} diff --git a/data/src/main/java/pokitmons/pokit/data/mapper/link/LinkMapper.kt b/data/src/main/java/pokitmons/pokit/data/mapper/link/LinkMapper.kt new file mode 100644 index 00000000..74fef49a --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/mapper/link/LinkMapper.kt @@ -0,0 +1,40 @@ +package pokitmons.pokit.data.mapper.link + +import pokitmons.pokit.data.model.link.response.GetLinkResponse +import pokitmons.pokit.data.model.link.response.GetLinksResponse +import pokitmons.pokit.domain.model.link.Link + +object LinkMapper { + fun mapperToLinks(linksResponse: GetLinksResponse): List { + return linksResponse.data.map { data -> + Link( + id = data.contentId, + categoryId = data.category.categoryId, + categoryName = data.category.categoryName, + data = data.data, + domain = data.domain, + title = data.title, + memo = data.memo, + alertYn = data.alertYn, + createdAt = data.createdAt, + isRead = data.isRead, + thumbnail = data.thumbNail + ) + } + } + + fun mapperToLink(linkResponse: GetLinkResponse): Link { + return Link( + id = linkResponse.contentId, + categoryId = linkResponse.categoryId, + categoryName = "", + data = linkResponse.data, + domain = "", + title = linkResponse.title, + memo = linkResponse.memo, + alertYn = linkResponse.alertYn, + createdAt = linkResponse.createdAt, + favorites = linkResponse.favorites + ) + } +} diff --git a/data/src/main/java/pokitmons/pokit/data/mapper/pokit/PokitMapper.kt b/data/src/main/java/pokitmons/pokit/data/mapper/pokit/PokitMapper.kt new file mode 100644 index 00000000..fdeddcbd --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/mapper/pokit/PokitMapper.kt @@ -0,0 +1,46 @@ +package pokitmons.pokit.data.mapper.pokit + +import pokitmons.pokit.data.model.pokit.response.GetPokitImagesResponseItem +import pokitmons.pokit.data.model.pokit.response.GetPokitResponse +import pokitmons.pokit.data.model.pokit.response.GetPokitsResponse +import pokitmons.pokit.domain.model.pokit.Pokit + +object PokitMapper { + private const val NOT_USE = 0 + + fun mapperToPokits(pokitsResponse: GetPokitsResponse): List { + return pokitsResponse.data.map { data -> + Pokit( + categoryId = data.categoryId, + userId = data.userId, + name = data.categoryName, + image = Pokit.Image( + id = data.categoryImage.imageId, + url = data.categoryImage.imageUrl + ), + createdAt = data.createdAt, + linkCount = data.contentCount + ) + } + } + + fun mapperToPokitImages(getPokitImagesResponse: List): List { + return getPokitImagesResponse.map { image -> + Pokit.Image(id = image.imageId, url = image.imageUrl) + } + } + + fun mapperToPokit(pokitResponse: GetPokitResponse): Pokit { + return Pokit( + categoryId = pokitResponse.categoryId, + userId = NOT_USE, + name = pokitResponse.categoryName, + image = Pokit.Image( + id = pokitResponse.categoryImage.imageId, + url = pokitResponse.categoryImage.imageUrl + ), + linkCount = NOT_USE, + createdAt = pokitResponse.createdAt + ) + } +} diff --git a/data/src/main/java/pokitmons/pokit/data/mapper/setting/SettingMapper.kt b/data/src/main/java/pokitmons/pokit/data/mapper/setting/SettingMapper.kt new file mode 100644 index 00000000..3080207b --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/mapper/setting/SettingMapper.kt @@ -0,0 +1,14 @@ +package pokitmons.pokit.data.mapper.setting + +import pokitmons.pokit.data.model.setting.response.EditNicknameResponse +import pokitmons.pokit.domain.model.setting.EditNicknameResult + +object SettingMapper { + fun mapperToEditNickname(editNicknameResponse: EditNicknameResponse): EditNicknameResult { + return EditNicknameResult( + id = editNicknameResponse.id, + nickname = editNicknameResponse.nickname, + email = editNicknameResponse.email + ) + } +} diff --git a/data/src/main/java/pokitmons/pokit/data/model/alert/GetAlertsResponse.kt b/data/src/main/java/pokitmons/pokit/data/model/alert/GetAlertsResponse.kt new file mode 100644 index 00000000..e55692a1 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/alert/GetAlertsResponse.kt @@ -0,0 +1,32 @@ +package pokitmons.pokit.data.model.alert + +import kotlinx.serialization.Serializable + +@Serializable +data class GetAlertsResponse( + val data: List = emptyList(), + val page: Int = 0, + val size: Int = 10, + val sort: List = emptyList(), + val hasNext: Boolean = true, +) { + @Serializable + data class Data( + val id: Int, + val userId: Int, + val contentId: Int, + val thumbNail: String, + val title: String, + val body: String, + val createdAt: String, + ) + + @Serializable + data class Sort( + val direction: String, + val nullHandling: String, + val ascending: Boolean, + val property: String, + val ignoreCase: Boolean, + ) +} diff --git a/data/src/main/java/pokitmons/pokit/data/model/auth/request/SignUpRequest.kt b/data/src/main/java/pokitmons/pokit/data/model/auth/request/SignUpRequest.kt index 016884a8..e1ddc46b 100644 --- a/data/src/main/java/pokitmons/pokit/data/model/auth/request/SignUpRequest.kt +++ b/data/src/main/java/pokitmons/pokit/data/model/auth/request/SignUpRequest.kt @@ -1,6 +1,9 @@ package pokitmons.pokit.data.model.auth.request +import kotlinx.serialization.Serializable + +@Serializable data class SignUpRequest( - val nickname: String, + val nickName: String, val interests: List, ) diff --git a/data/src/main/java/pokitmons/pokit/data/model/auth/response/SignUpResponse.kt b/data/src/main/java/pokitmons/pokit/data/model/auth/response/SignUpResponse.kt index f98bdc98..c86dc03a 100644 --- a/data/src/main/java/pokitmons/pokit/data/model/auth/response/SignUpResponse.kt +++ b/data/src/main/java/pokitmons/pokit/data/model/auth/response/SignUpResponse.kt @@ -1,5 +1,10 @@ package pokitmons.pokit.data.model.auth.response +import kotlinx.serialization.Serializable + +@Serializable data class SignUpResponse( - val userId: Int, + val id: Int, + val email: String, + val nickname: String, ) diff --git a/data/src/main/java/pokitmons/pokit/data/model/home/remind/Category.kt b/data/src/main/java/pokitmons/pokit/data/model/home/remind/Category.kt new file mode 100644 index 00000000..5c119e7b --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/home/remind/Category.kt @@ -0,0 +1,9 @@ +package pokitmons.pokit.data.model.home.remind + +import kotlinx.serialization.Serializable + +@Serializable +data class Category( + val categoryId: Int, + val categoryName: String, +) diff --git a/data/src/main/java/pokitmons/pokit/data/model/home/remind/Remind.kt b/data/src/main/java/pokitmons/pokit/data/model/home/remind/Remind.kt new file mode 100644 index 00000000..21cc45a3 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/home/remind/Remind.kt @@ -0,0 +1,15 @@ +package pokitmons.pokit.data.model.home.remind + +import kotlinx.serialization.Serializable + +@Serializable +data class Remind( + val category: Category, + val contentId: Int, + val createdAt: String, + val data: String, + val domain: String, + val isRead: Boolean, + val thumbNail: String, + val title: String, +) diff --git a/data/src/main/java/pokitmons/pokit/data/model/home/remind/RemindRequest.kt b/data/src/main/java/pokitmons/pokit/data/model/home/remind/RemindRequest.kt new file mode 100644 index 00000000..6aec9476 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/home/remind/RemindRequest.kt @@ -0,0 +1,11 @@ +package pokitmons.pokit.data.model.home.remind + +import kotlinx.serialization.Serializable +import pokitmons.pokit.domain.model.pokit.PokitsSort + +@Serializable +data class RemindRequest( + val size: Int = 10, + val page: Int = 0, + val sort: PokitsSort = PokitsSort.RECENT, +) diff --git a/data/src/main/java/pokitmons/pokit/data/model/home/remind/RemindResponse.kt b/data/src/main/java/pokitmons/pokit/data/model/home/remind/RemindResponse.kt new file mode 100644 index 00000000..d2789fbf --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/home/remind/RemindResponse.kt @@ -0,0 +1,12 @@ +package pokitmons.pokit.data.model.home.remind + +import kotlinx.serialization.Serializable + +@Serializable +data class RemindResponse( + val data: List = emptyList(), + val hasNext: Boolean = false, + val page: Int = 0, + val size: Int = 10, + val sort: List = emptyList(), +) diff --git a/data/src/main/java/pokitmons/pokit/data/model/home/remind/Sort.kt b/data/src/main/java/pokitmons/pokit/data/model/home/remind/Sort.kt new file mode 100644 index 00000000..48e4f07b --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/home/remind/Sort.kt @@ -0,0 +1,12 @@ +package pokitmons.pokit.data.model.home.remind + +import kotlinx.serialization.Serializable + +@Serializable +data class Sort( + val ascending: Boolean, + val direction: String, + val ignoreCase: Boolean, + val nullHandling: String, + val property: String, +) diff --git a/data/src/main/java/pokitmons/pokit/data/model/link/request/ModifyLinkRequest.kt b/data/src/main/java/pokitmons/pokit/data/model/link/request/ModifyLinkRequest.kt new file mode 100644 index 00000000..7144bae4 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/link/request/ModifyLinkRequest.kt @@ -0,0 +1,13 @@ +package pokitmons.pokit.data.model.link.request + +import kotlinx.serialization.Serializable + +@Serializable +class ModifyLinkRequest( + val data: String = "", + val title: String = "", + val categoryId: Int = 0, + val memo: String = "", + val alertYn: String = "", + val thumbNail: String = "", +) diff --git a/data/src/main/java/pokitmons/pokit/data/model/link/response/ApplyBookmarkResponse.kt b/data/src/main/java/pokitmons/pokit/data/model/link/response/ApplyBookmarkResponse.kt new file mode 100644 index 00000000..bf54df13 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/link/response/ApplyBookmarkResponse.kt @@ -0,0 +1,6 @@ +package pokitmons.pokit.data.model.link.response + +import kotlinx.serialization.Serializable + +@Serializable +data class ApplyBookmarkResponse(val contentId: Int = 0) diff --git a/data/src/main/java/pokitmons/pokit/data/model/link/response/GetLinkResponse.kt b/data/src/main/java/pokitmons/pokit/data/model/link/response/GetLinkResponse.kt new file mode 100644 index 00000000..b484c9bf --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/link/response/GetLinkResponse.kt @@ -0,0 +1,15 @@ +package pokitmons.pokit.data.model.link.response + +import kotlinx.serialization.Serializable + +@Serializable +data class GetLinkResponse( + val contentId: Int = 0, + val categoryId: Int = 0, + val data: String = "", + val title: String = "", + val memo: String = "", + val alertYn: String = "", + val createdAt: String = "", + val favorites: Boolean = true, +) diff --git a/data/src/main/java/pokitmons/pokit/data/model/link/response/GetLinksResponse.kt b/data/src/main/java/pokitmons/pokit/data/model/link/response/GetLinksResponse.kt new file mode 100644 index 00000000..1b7f2320 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/link/response/GetLinksResponse.kt @@ -0,0 +1,41 @@ +package pokitmons.pokit.data.model.link.response + +import kotlinx.serialization.Serializable + +@Serializable +data class GetLinksResponse( + val data: List = emptyList(), + val page: Int = 0, + val size: Int = 10, + val sort: List = emptyList(), + val hasNext: Boolean = true, +) { + @Serializable + data class Data( + val contentId: Int, + val category: Category, + val data: String, + val domain: String, + val title: String, + val memo: String, + val alertYn: String, + val createdAt: String, + val isRead: Boolean, + val thumbNail: String, + ) + + @Serializable + data class Category( + val categoryId: Int, + val categoryName: String, + ) + + @Serializable + data class Sort( + val direction: String, + val nullHandling: String, + val ascending: Boolean, + val property: String, + val ignoreCase: Boolean, + ) +} diff --git a/data/src/main/java/pokitmons/pokit/data/model/link/response/LinkCardResponse.kt b/data/src/main/java/pokitmons/pokit/data/model/link/response/LinkCardResponse.kt new file mode 100644 index 00000000..41abd42d --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/link/response/LinkCardResponse.kt @@ -0,0 +1,7 @@ +package pokitmons.pokit.data.model.link.response + +data class LinkCardResponse( + val url: String = "", + val title: String = "", + val image: String? = null, +) diff --git a/data/src/main/java/pokitmons/pokit/data/model/link/response/ModifyLinkResponse.kt b/data/src/main/java/pokitmons/pokit/data/model/link/response/ModifyLinkResponse.kt new file mode 100644 index 00000000..b2ead0e6 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/link/response/ModifyLinkResponse.kt @@ -0,0 +1,15 @@ +package pokitmons.pokit.data.model.link.response + +import kotlinx.serialization.Serializable + +@Serializable +data class ModifyLinkResponse( + val contentId: Int = 0, + val categoryId: Int = 0, + val data: String = "", + val title: String = "", + val memo: String = "", + val alertYn: String = "", + val createdAt: String = "", + val favorites: Boolean = true, +) diff --git a/data/src/main/java/pokitmons/pokit/data/model/pokit/request/CreatePokitRequest.kt b/data/src/main/java/pokitmons/pokit/data/model/pokit/request/CreatePokitRequest.kt new file mode 100644 index 00000000..93cf3847 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/pokit/request/CreatePokitRequest.kt @@ -0,0 +1,9 @@ +package pokitmons.pokit.data.model.pokit.request + +import kotlinx.serialization.Serializable + +@Serializable +data class CreatePokitRequest( + val categoryName: String = "", + val categoryImageId: Int = 0, +) diff --git a/data/src/main/java/pokitmons/pokit/data/model/pokit/request/GetPokitsRequest.kt b/data/src/main/java/pokitmons/pokit/data/model/pokit/request/GetPokitsRequest.kt new file mode 100644 index 00000000..0142195b --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/pokit/request/GetPokitsRequest.kt @@ -0,0 +1,10 @@ +package pokitmons.pokit.data.model.pokit.request + +import pokitmons.pokit.domain.model.pokit.PokitsSort + +data class GetPokitsRequest( + val filterUncategoriezd: Boolean = true, + val size: Int = 10, + val page: Int = 0, + val sort: PokitsSort = PokitsSort.RECENT, +) diff --git a/data/src/main/java/pokitmons/pokit/data/model/pokit/request/ModifyPokitRequest.kt b/data/src/main/java/pokitmons/pokit/data/model/pokit/request/ModifyPokitRequest.kt new file mode 100644 index 00000000..79bce0ef --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/pokit/request/ModifyPokitRequest.kt @@ -0,0 +1,9 @@ +package pokitmons.pokit.data.model.pokit.request + +import kotlinx.serialization.Serializable + +@Serializable +data class ModifyPokitRequest( + val categoryName: String = "", + val categoryImageId: Int = 0, +) diff --git a/data/src/main/java/pokitmons/pokit/data/model/pokit/response/CreatePokitResponse.kt b/data/src/main/java/pokitmons/pokit/data/model/pokit/response/CreatePokitResponse.kt new file mode 100644 index 00000000..513a2bda --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/pokit/response/CreatePokitResponse.kt @@ -0,0 +1,16 @@ +package pokitmons.pokit.data.model.pokit.response + +import kotlinx.serialization.Serializable + +@Serializable +data class CreatePokitResponse( + val categoryId: Int = 0, + val categoryName: String = "", + val categoryImage: Image = Image(), +) { + @Serializable + data class Image( + val imageId: Int = 0, + val imageUrl: String = "", + ) +} diff --git a/data/src/main/java/pokitmons/pokit/data/model/pokit/response/GetPokitCountResponse.kt b/data/src/main/java/pokitmons/pokit/data/model/pokit/response/GetPokitCountResponse.kt new file mode 100644 index 00000000..bd49c046 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/pokit/response/GetPokitCountResponse.kt @@ -0,0 +1,8 @@ +package pokitmons.pokit.data.model.pokit.response + +import kotlinx.serialization.Serializable + +@Serializable +data class GetPokitCountResponse( + val categoryTotalCount: Int = 0, +) diff --git a/data/src/main/java/pokitmons/pokit/data/model/pokit/response/GetPokitImagesResponseItem.kt b/data/src/main/java/pokitmons/pokit/data/model/pokit/response/GetPokitImagesResponseItem.kt new file mode 100644 index 00000000..a0e89792 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/pokit/response/GetPokitImagesResponseItem.kt @@ -0,0 +1,9 @@ +package pokitmons.pokit.data.model.pokit.response + +import kotlinx.serialization.Serializable + +@Serializable +data class GetPokitImagesResponseItem( + val imageId: Int, + val imageUrl: String, +) diff --git a/data/src/main/java/pokitmons/pokit/data/model/pokit/response/GetPokitResponse.kt b/data/src/main/java/pokitmons/pokit/data/model/pokit/response/GetPokitResponse.kt new file mode 100644 index 00000000..2ea6ccb3 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/pokit/response/GetPokitResponse.kt @@ -0,0 +1,17 @@ +package pokitmons.pokit.data.model.pokit.response + +import kotlinx.serialization.Serializable + +@Serializable +data class GetPokitResponse( + val categoryId: Int = 0, + val categoryName: String = "", + val categoryImage: Image = Image(), + val createdAt: String = "", +) { + @Serializable + data class Image( + val imageId: Int = 0, + val imageUrl: String = "", + ) +} diff --git a/data/src/main/java/pokitmons/pokit/data/model/pokit/response/GetPokitsResponse.kt b/data/src/main/java/pokitmons/pokit/data/model/pokit/response/GetPokitsResponse.kt new file mode 100644 index 00000000..87790c1b --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/pokit/response/GetPokitsResponse.kt @@ -0,0 +1,37 @@ +package pokitmons.pokit.data.model.pokit.response + +import kotlinx.serialization.Serializable + +@Serializable +data class GetPokitsResponse( + val data: List = emptyList(), + val page: Int = 10, + val size: Int = 0, + val sort: List = emptyList(), + val hasNext: Boolean = false, +) { + @Serializable + data class Data( + val categoryId: Int, + val userId: Int, + val categoryName: String, + val categoryImage: PokitImage, + val contentCount: Int, + val createdAt: String, + ) + + @Serializable + data class PokitImage( + val imageId: Int, + val imageUrl: String, + ) + + @Serializable + data class Sort( + val direction: String, + val nullHandling: String, + val ascending: Boolean, + val property: String, + val ignoreCase: Boolean, + ) +} diff --git a/data/src/main/java/pokitmons/pokit/data/model/pokit/response/ModifyPokitResponse.kt b/data/src/main/java/pokitmons/pokit/data/model/pokit/response/ModifyPokitResponse.kt new file mode 100644 index 00000000..81c9293b --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/pokit/response/ModifyPokitResponse.kt @@ -0,0 +1,16 @@ +package pokitmons.pokit.data.model.pokit.response + +import kotlinx.serialization.Serializable + +@Serializable +data class ModifyPokitResponse( + val categoryId: Int = 0, + val categoryName: String = "", + val categoryImage: Image = Image(), +) { + @Serializable + data class Image( + val imageId: Int = 0, + val imageUrl: String = "", + ) +} diff --git a/data/src/main/java/pokitmons/pokit/data/model/setting/reqeust/EditNicknameRequest.kt b/data/src/main/java/pokitmons/pokit/data/model/setting/reqeust/EditNicknameRequest.kt new file mode 100644 index 00000000..edfa5e55 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/setting/reqeust/EditNicknameRequest.kt @@ -0,0 +1,8 @@ +package pokitmons.pokit.data.model.setting.reqeust + +import kotlinx.serialization.Serializable + +@Serializable +data class EditNicknameRequest( + val nickname: String, +) diff --git a/data/src/main/java/pokitmons/pokit/data/model/setting/response/EditNicknameResponse.kt b/data/src/main/java/pokitmons/pokit/data/model/setting/response/EditNicknameResponse.kt new file mode 100644 index 00000000..75fdd27e --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/setting/response/EditNicknameResponse.kt @@ -0,0 +1,10 @@ +package pokitmons.pokit.data.model.setting.response + +import kotlinx.serialization.Serializable + +@Serializable +data class EditNicknameResponse( + val email: String, + val id: Int, + val nickname: String, +) diff --git a/data/src/main/java/pokitmons/pokit/data/repository/alert/AlertRepositoryImpl.kt b/data/src/main/java/pokitmons/pokit/data/repository/alert/AlertRepositoryImpl.kt new file mode 100644 index 00000000..258b9ed0 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/repository/alert/AlertRepositoryImpl.kt @@ -0,0 +1,32 @@ +package pokitmons.pokit.data.repository.alert + +import pokitmons.pokit.data.datasource.remote.alert.AlertDataSource +import pokitmons.pokit.data.mapper.alert.AlertMapper +import pokitmons.pokit.data.model.common.parseErrorResult +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.alert.Alarm +import pokitmons.pokit.domain.repository.alert.AlertRepository +import javax.inject.Inject + +class AlertRepositoryImpl @Inject constructor( + private val dataSource: AlertDataSource, +) : AlertRepository { + override suspend fun getAlerts(page: Int, size: Int): PokitResult> { + return runCatching { + val response = dataSource.getAlerts(page = page, size = size) + val mappedResponse = AlertMapper.mapperToAlarmList(response) + PokitResult.Success(result = mappedResponse) + }.getOrElse { throwable -> + parseErrorResult(throwable) + } + } + + override suspend fun deleteAlert(alertId: Int): PokitResult { + return runCatching { + dataSource.deleteAlert(alertId) + PokitResult.Success(result = Unit) + }.getOrElse { throwable -> + parseErrorResult(throwable) + } + } +} diff --git a/data/src/main/java/pokitmons/pokit/data/repository/auth/AuthRepositoryImpl.kt b/data/src/main/java/pokitmons/pokit/data/repository/auth/AuthRepositoryImpl.kt index ff31557f..44dff281 100644 --- a/data/src/main/java/pokitmons/pokit/data/repository/auth/AuthRepositoryImpl.kt +++ b/data/src/main/java/pokitmons/pokit/data/repository/auth/AuthRepositoryImpl.kt @@ -3,16 +3,18 @@ package pokitmons.pokit.data.repository.auth import pokitmons.pokit.data.datasource.remote.auth.AuthDataSource import pokitmons.pokit.data.mapper.auth.AuthMapper import pokitmons.pokit.data.model.auth.request.SNSLoginRequest +import pokitmons.pokit.data.model.auth.request.SignUpRequest import pokitmons.pokit.data.model.auth.response.DuplicateNicknameResponse import pokitmons.pokit.data.model.auth.response.SNSLoginResponse +import pokitmons.pokit.data.model.auth.response.SignUpResponse import pokitmons.pokit.data.model.common.parseErrorResult import pokitmons.pokit.domain.commom.PokitResult import pokitmons.pokit.domain.model.auth.DuplicateNicknameResult import pokitmons.pokit.domain.model.auth.SNSLoginResult +import pokitmons.pokit.domain.model.auth.SignUpResult import pokitmons.pokit.domain.repository.auth.AuthRepository import javax.inject.Inject -// TODO getOrElse 반복되는 로직 함수화 class AuthRepositoryImpl @Inject constructor( private val remoteAuthDataSource: AuthDataSource, ) : AuthRepository { @@ -38,4 +40,14 @@ class AuthRepositoryImpl @Inject constructor( parseErrorResult(throwable) } } + + override suspend fun signUp(nickname: String, categories: List): PokitResult { + return runCatching { + val signUpResponse: SignUpResponse = remoteAuthDataSource.signUp(SignUpRequest(nickName = nickname, interests = categories)) + val signUpMapper: SignUpResult = AuthMapper.mapperToSignUp(signUpResponse) + PokitResult.Success(signUpMapper) + }.getOrElse { throwable -> + parseErrorResult(throwable) + } + } } diff --git a/data/src/main/java/pokitmons/pokit/data/repository/home/remind/RemindRepositoryImpl.kt b/data/src/main/java/pokitmons/pokit/data/repository/home/remind/RemindRepositoryImpl.kt new file mode 100644 index 00000000..5cf6ddce --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/repository/home/remind/RemindRepositoryImpl.kt @@ -0,0 +1,58 @@ +package pokitmons.pokit.data.repository.home.remind + +import pokitmons.pokit.data.datasource.remote.home.remind.RemindDataSource +import pokitmons.pokit.data.mapper.home.home.RemindMapper +import pokitmons.pokit.data.model.common.parseErrorResult +import pokitmons.pokit.data.model.home.remind.RemindRequest +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.home.remind.RemindResult +import pokitmons.pokit.domain.model.pokit.PokitsSort +import pokitmons.pokit.domain.repository.home.remind.RemindRepository +import javax.inject.Inject + +class RemindRepositoryImpl @Inject constructor(private val remindDataSource: RemindDataSource) : RemindRepository { + override suspend fun getUnReadContents( + filterUncategorized: Boolean, + size: Int, + page: Int, + sort: PokitsSort, + ): PokitResult> { + return runCatching { + val response = remindDataSource.getUnreadContents(RemindRequest()) + val remindResponse = RemindMapper.mapperToRemind(response) + PokitResult.Success(remindResponse) + }.getOrElse { throwable -> + parseErrorResult(throwable) + } + } + + override suspend fun getTodayContents( + filterUncategorized: Boolean, + size: Int, + page: Int, + sort: PokitsSort, + ): PokitResult> { + return runCatching { + val response = remindDataSource.getTodayContents(RemindRequest()) + val remindResponse = RemindMapper.mapperToTodayContents(response) + PokitResult.Success(remindResponse) + }.getOrElse { throwable -> + parseErrorResult(throwable) + } + } + + override suspend fun getBookmarkContents( + filterUncategorized: Boolean, + size: Int, + page: Int, + sort: PokitsSort, + ): PokitResult> { + return runCatching { + val response = remindDataSource.getBookmarkContents(RemindRequest()) + val remindResponse = RemindMapper.mapperToRemind(response) + PokitResult.Success(remindResponse) + }.getOrElse { throwable -> + parseErrorResult(throwable) + } + } +} diff --git a/data/src/main/java/pokitmons/pokit/data/repository/link/LinkRepositoryImpl.kt b/data/src/main/java/pokitmons/pokit/data/repository/link/LinkRepositoryImpl.kt new file mode 100644 index 00000000..abff80f5 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/repository/link/LinkRepositoryImpl.kt @@ -0,0 +1,170 @@ +package pokitmons.pokit.data.repository.link + +import pokitmons.pokit.data.datasource.remote.link.LinkDataSource +import pokitmons.pokit.data.mapper.link.LinkMapper +import pokitmons.pokit.data.model.common.parseErrorResult +import pokitmons.pokit.data.model.link.request.ModifyLinkRequest +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.link.Link +import pokitmons.pokit.domain.model.link.LinkCard +import pokitmons.pokit.domain.model.link.LinksSort +import pokitmons.pokit.domain.repository.link.LinkRepository +import javax.inject.Inject + +class LinkRepositoryImpl @Inject constructor( + private val dataSource: LinkDataSource, +) : LinkRepository { + override suspend fun getLinks( + categoryId: Int, + size: Int, + page: Int, + sort: LinksSort, + isRead: Boolean, + favorite: Boolean, + startDate: String?, + endDate: String?, + categoryIds: List?, + ): PokitResult> { + return runCatching { + val response = dataSource.getLinks( + categoryId = categoryId, + size = size, + page = page, + sort = listOf(sort.value), + isRead = isRead, + favorites = favorite, + startDate = startDate, + endDate = endDate, + categoryIds = categoryIds + ) + val mappedResponse = LinkMapper.mapperToLinks(response) + PokitResult.Success(mappedResponse) + }.getOrElse { throwable -> + parseErrorResult(throwable) + } + } + + override suspend fun searchLinks( + page: Int, + size: Int, + sort: List, + isRead: Boolean, + favorites: Boolean, + startDate: String?, + endDate: String?, + categoryIds: List?, + searchWord: String, + ): PokitResult> { + return runCatching { + val response = dataSource.searchLinks( + page = page, + size = size, + sort = sort, + isRead = isRead, + favorites = favorites, + startDate = startDate, + endDate = endDate, + categoryIds = categoryIds, + searchWord = searchWord + ) + val mappedResponse = LinkMapper.mapperToLinks(response) + PokitResult.Success(mappedResponse) + }.getOrElse { throwable -> + parseErrorResult(throwable) + } + } + + override suspend fun deleteLink(linkId: Int): PokitResult { + return runCatching { + dataSource.deleteLink(linkId) + PokitResult.Success(linkId) + }.getOrElse { throwable -> + parseErrorResult(throwable) + } + } + + override suspend fun getLink(linkId: Int): PokitResult { + return runCatching { + val response = dataSource.getLink(linkId) + val mappedResponse = LinkMapper.mapperToLink(response) + PokitResult.Success(mappedResponse) + }.getOrElse { throwable -> + parseErrorResult(throwable) + } + } + + override suspend fun modifyLink( + linkId: Int, + data: String, + title: String, + categoryId: Int, + memo: String, + alertYn: String, + thumbNail: String, + ): PokitResult { + return runCatching { + val modifyLinkRequest = ModifyLinkRequest( + data = data, + title = title, + categoryId = categoryId, + memo = memo, + alertYn = alertYn, + thumbNail = thumbNail + ) + val response = dataSource.modifyLink(contentId = linkId, modifyLinkRequest = modifyLinkRequest) + PokitResult.Success(response.contentId) + }.getOrElse { throwable -> + parseErrorResult(throwable) + } + } + + override suspend fun createLink(data: String, title: String, categoryId: Int, memo: String, alertYn: String, thumbNail: String): PokitResult { + return runCatching { + val createLinkRequest = ModifyLinkRequest( + data = data, + title = title, + categoryId = categoryId, + memo = memo, + alertYn = alertYn, + thumbNail = thumbNail + ) + val response = dataSource.createLink(createLinkRequest = createLinkRequest) + PokitResult.Success(response.contentId) + }.getOrElse { throwable -> + parseErrorResult(throwable) + } + } + + override suspend fun setBookmark(linkId: Int, bookmarked: Boolean): PokitResult { + return runCatching { + dataSource.setBookmark(contentId = linkId, bookmarked = bookmarked) + PokitResult.Success(Unit) + }.getOrElse { throwable -> + parseErrorResult(throwable) + } + } + + override suspend fun getLinkCard(url: String): PokitResult { + return runCatching { + val response = dataSource.getLinkCard(url) + val mappedResponse = LinkCard( + url = response.url, + title = response.title, + thumbnailUrl = response.image + ) + PokitResult.Success(result = mappedResponse) + }.getOrElse { throwable -> + parseErrorResult(throwable) + } + } + + override suspend fun getUncategorizedLinks(size: Int, page: Int, sort: LinksSort): PokitResult> { + return runCatching { + val response = dataSource.getUncategorizedLinks(size = size, page = page, sort = listOf(sort.value)) + val mappedResponse = LinkMapper.mapperToLinks(response) + PokitResult.Success(mappedResponse) + }.getOrElse { throwable -> + parseErrorResult(throwable) + } + } +} diff --git a/data/src/main/java/pokitmons/pokit/data/repository/pokit/PokitRepositoryImpl.kt b/data/src/main/java/pokitmons/pokit/data/repository/pokit/PokitRepositoryImpl.kt new file mode 100644 index 00000000..3ea46299 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/repository/pokit/PokitRepositoryImpl.kt @@ -0,0 +1,97 @@ +package pokitmons.pokit.data.repository.pokit + +import pokitmons.pokit.data.datasource.remote.pokit.PokitDataSource +import pokitmons.pokit.data.mapper.pokit.PokitMapper +import pokitmons.pokit.data.model.common.parseErrorResult +import pokitmons.pokit.data.model.pokit.request.CreatePokitRequest +import pokitmons.pokit.data.model.pokit.request.GetPokitsRequest +import pokitmons.pokit.data.model.pokit.request.ModifyPokitRequest +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.pokit.Pokit +import pokitmons.pokit.domain.model.pokit.PokitsSort +import pokitmons.pokit.domain.repository.pokit.PokitRepository +import javax.inject.Inject + +class PokitRepositoryImpl @Inject constructor( + private val pokitDataSource: PokitDataSource, +) : PokitRepository { + override suspend fun getPokits( + filterUncategorized: Boolean, + size: Int, + page: Int, + sort: PokitsSort, + ): PokitResult> { + return runCatching { + val request = GetPokitsRequest( + filterUncategoriezd = filterUncategorized, + size = size, + page = page, + sort = sort + ) + val response = pokitDataSource.getPokits(request) + val mappedResponse = PokitMapper.mapperToPokits(response) + PokitResult.Success(mappedResponse) + }.getOrElse { throwable -> + parseErrorResult(throwable) + } + } + + override suspend fun createPokit(name: String, imageId: Int): PokitResult { + return runCatching { + val request = CreatePokitRequest(categoryName = name, categoryImageId = imageId) + val response = pokitDataSource.createPokit(request) + val pokitId = response.categoryId + PokitResult.Success(pokitId) + }.getOrElse { throwable -> + parseErrorResult(throwable) + } + } + + override suspend fun modifyPokit(pokitId: Int, name: String, imageId: Int): PokitResult { + return runCatching { + val request = ModifyPokitRequest(categoryName = name, categoryImageId = imageId) + val response = pokitDataSource.modifyPokit(pokitId, request) + PokitResult.Success(response.categoryId) + }.getOrElse { throwable -> + parseErrorResult(throwable) + } + } + + override suspend fun getPokitImages(): PokitResult> { + return runCatching { + val response = pokitDataSource.getPokitImages() + val mappedResponse = PokitMapper.mapperToPokitImages(response) + PokitResult.Success(mappedResponse) + }.getOrElse { throwable -> + parseErrorResult(throwable) + } + } + + override suspend fun getPokit(pokitId: Int): PokitResult { + return runCatching { + val response = pokitDataSource.getPokit(pokitId) + val mappedResponse = PokitMapper.mapperToPokit(response) + PokitResult.Success(mappedResponse) + }.getOrElse { throwable -> + parseErrorResult(throwable) + } + } + + override suspend fun deletePokit(pokitId: Int): PokitResult { + return runCatching { + pokitDataSource.deletePokit(pokitId) + PokitResult.Success(result = Unit) + }.getOrElse { throwable -> + parseErrorResult(throwable) + } + } + + override suspend fun getPokitCount(): PokitResult { + return kotlin.runCatching { + val response = pokitDataSource.getPokitCount() + PokitResult.Success(result = response.categoryTotalCount) + }.getOrElse { throwable -> + parseErrorResult(throwable) + } + } +} diff --git a/data/src/main/java/pokitmons/pokit/data/repository/search/SearchRepositoryImpl.kt b/data/src/main/java/pokitmons/pokit/data/repository/search/SearchRepositoryImpl.kt new file mode 100644 index 00000000..5ce7c2b8 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/repository/search/SearchRepositoryImpl.kt @@ -0,0 +1,34 @@ +package pokitmons.pokit.data.repository.search + +import kotlinx.coroutines.flow.Flow +import pokitmons.pokit.data.datasource.local.search.SearchDataSource +import pokitmons.pokit.domain.repository.search.SearchRepository +import javax.inject.Inject + +class SearchRepositoryImpl @Inject constructor( + private val dataSource: SearchDataSource, +) : SearchRepository { + override fun getRecentSearchWords(): Flow> { + return dataSource.getSearchWord() + } + + override suspend fun removeSearchWord(word: String) { + dataSource.removeSearchWord(word) + } + + override suspend fun removeAllSearchWords() { + dataSource.removeAllSearchWords() + } + + override suspend fun setUseRecentSearchWord(use: Boolean): Boolean { + return dataSource.setUseRecentSearchWord(use = use) + } + + override fun getUseRecentSearchWord(): Flow { + return dataSource.getUseRecentSearchWord() + } + + override suspend fun addRecentSearchWord(word: String) { + dataSource.addSearchWord(word) + } +} diff --git a/data/src/main/java/pokitmons/pokit/data/repository/setting/SettingRepositoryImpl.kt b/data/src/main/java/pokitmons/pokit/data/repository/setting/SettingRepositoryImpl.kt new file mode 100644 index 00000000..aea7447f --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/repository/setting/SettingRepositoryImpl.kt @@ -0,0 +1,25 @@ +package pokitmons.pokit.data.repository.setting + +import pokitmons.pokit.data.datasource.remote.setting.SettingDataSource +import pokitmons.pokit.data.mapper.setting.SettingMapper +import pokitmons.pokit.data.model.common.parseErrorResult +import pokitmons.pokit.data.model.setting.reqeust.EditNicknameRequest +import pokitmons.pokit.data.model.setting.response.EditNicknameResponse +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.setting.EditNicknameResult +import pokitmons.pokit.domain.repository.setting.SettingRepository +import javax.inject.Inject + +class SettingRepositoryImpl @Inject constructor( + private val remoteSettingDataSource: SettingDataSource, +) : SettingRepository { + override suspend fun editNickname(nickname: String): PokitResult { + return runCatching { + val editNicknameResponse: EditNicknameResponse = remoteSettingDataSource.editNickname(EditNicknameRequest(nickname)) + val editNicknameMapper = SettingMapper.mapperToEditNickname(editNicknameResponse) + PokitResult.Success(editNicknameMapper) + }.getOrElse { throwable -> + parseErrorResult(throwable) + } + } +} diff --git a/data/src/main/java/pokitmons/pokit/data/room/dao/SearchWordDao.kt b/data/src/main/java/pokitmons/pokit/data/room/dao/SearchWordDao.kt new file mode 100644 index 00000000..d1506b5f --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/room/dao/SearchWordDao.kt @@ -0,0 +1,23 @@ +package pokitmons.pokit.data.room.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import kotlinx.coroutines.flow.Flow +import pokitmons.pokit.data.room.entity.SearchWord + +@Dao +interface SearchWordDao { + @Query("SELECT word from SearchWord order by searchedAt desc limit 10") + fun getRecentSearchWords(): Flow> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun addSearchWord(searchWord: SearchWord) + + @Query("DELETE from SearchWord where word = :word") + suspend fun removeSearchWord(word: String) + + @Query("DELETE from SearchWord") + suspend fun removeAllSearchWords() +} diff --git a/data/src/main/java/pokitmons/pokit/data/room/database/AppDatabase.kt b/data/src/main/java/pokitmons/pokit/data/room/database/AppDatabase.kt new file mode 100644 index 00000000..0f3e4b39 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/room/database/AppDatabase.kt @@ -0,0 +1,11 @@ +package pokitmons.pokit.data.room.database + +import androidx.room.Database +import androidx.room.RoomDatabase +import pokitmons.pokit.data.room.dao.SearchWordDao +import pokitmons.pokit.data.room.entity.SearchWord + +@Database(entities = [SearchWord::class], version = 1) +abstract class AppDatabase : RoomDatabase() { + abstract fun searchWordDao(): SearchWordDao +} diff --git a/data/src/main/java/pokitmons/pokit/data/room/entity/SearchWord.kt b/data/src/main/java/pokitmons/pokit/data/room/entity/SearchWord.kt new file mode 100644 index 00000000..aedcc90f --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/room/entity/SearchWord.kt @@ -0,0 +1,10 @@ +package pokitmons.pokit.data.room.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity + +@Entity(primaryKeys = ["word"]) +data class SearchWord( + @ColumnInfo("word") val word: String, + @ColumnInfo("searchedAt") val searchedAt: String, +) diff --git a/data/src/test/java/pokitmons/pokit/data/datasource/RemoteAlertDataSourceTest.kt b/data/src/test/java/pokitmons/pokit/data/datasource/RemoteAlertDataSourceTest.kt new file mode 100644 index 00000000..a830d7f6 --- /dev/null +++ b/data/src/test/java/pokitmons/pokit/data/datasource/RemoteAlertDataSourceTest.kt @@ -0,0 +1,48 @@ +package pokitmons.pokit.data.datasource + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.mockk.coEvery +import io.mockk.mockk +import pokitmons.pokit.data.api.AlertApi +import pokitmons.pokit.data.datasource.remote.alert.RemoteAlertDataSource +import pokitmons.pokit.data.model.alert.GetAlertsResponse + +val alertApi: AlertApi = mockk() + +class RemoteAlertDataSourceTest : DescribeSpec({ + val remoteAlertDataSource = RemoteAlertDataSource(alertApi) + describe("알림 목록 조회시") { + context("알림 목록 조회가 정상적으로 수행되면") { + coEvery { alertApi.getAlerts(page = 0, size = 0) } returns GetAlertsResponse() + it("알림 목록이 반환된다.") { + val response = remoteAlertDataSource.getAlerts(page = 0, size = 0) + response.shouldBeInstanceOf() + } + } + + context("에러가 발생했다면") { + coEvery { alertApi.getAlerts(page = 0, size = 0) } throws IllegalArgumentException("error") + it("동일한 에러가 발생한다.") { + val exception = shouldThrow { + remoteAlertDataSource.getAlerts(page = 0, size = 0) + } + exception.message shouldBe "error" + } + } + } + + describe("알림 제거시") { + context("에러가 발생했다면") { + coEvery { alertApi.deleteAlert(alertId = 0) } throws IllegalArgumentException("error") + it("동일한 에러가 발생한다.") { + val exception = shouldThrow { + remoteAlertDataSource.deleteAlert(alertId = 0) + } + exception.message shouldBe "error" + } + } + } +}) diff --git a/data/src/test/java/pokitmons/pokit/data/datasource/RemoteLinkDataSourceTest.kt b/data/src/test/java/pokitmons/pokit/data/datasource/RemoteLinkDataSourceTest.kt new file mode 100644 index 00000000..011fcc28 --- /dev/null +++ b/data/src/test/java/pokitmons/pokit/data/datasource/RemoteLinkDataSourceTest.kt @@ -0,0 +1,58 @@ +package pokitmons.pokit.data.datasource + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.mockk.coEvery +import io.mockk.mockk +import pokitmons.pokit.data.api.LinkApi +import pokitmons.pokit.data.datasource.remote.link.RemoteLinkDataSource +import pokitmons.pokit.data.model.link.response.GetLinksResponse + +val linkApi: LinkApi = mockk() + +class RemoteLinkDataSourceTest : DescribeSpec({ + val remoteLinkDataSource = RemoteLinkDataSource(linkApi) + describe("링크 목록 조회시") { + context("링크 목록 조회가 정상적으로 수신되면") { + coEvery { linkApi.getLinks() } returns GetLinksResponse() + it("링크 목록이 반환된다.") { + val response = remoteLinkDataSource.getLinks() + response.shouldBeInstanceOf() + } + } + + context("에러가 발생했다면") { + coEvery { linkApi.getLinks() } throws IllegalArgumentException("error") + it("동일한 에러가 발생한다.") { + val exception = shouldThrow { + remoteLinkDataSource.getLinks() + } + exception.message shouldBe "error" + } + } + } + + describe("북마크 변경") { + context("북마크 취소 도중 예외가 발생했다면") { + coEvery { linkApi.cancelBookmark(0) } throws IllegalArgumentException("error") + it("동일한 에러가 발생한다.") { + val exception = shouldThrow { + remoteLinkDataSource.setBookmark(contentId = 0, bookmarked = false) + } + exception.message shouldBe "error" + } + } + + context("북마크 등록 도중 예외가 발생했다면") { + coEvery { linkApi.applyBookmark(0) } throws IllegalArgumentException("error") + it("동일한 에러가 발생한다.") { + val exception = shouldThrow { + remoteLinkDataSource.setBookmark(contentId = 0, bookmarked = true) + } + exception.message shouldBe "error" + } + } + } +}) diff --git a/data/src/test/java/pokitmons/pokit/data/datasource/RemotePokitDataSourceTest.kt b/data/src/test/java/pokitmons/pokit/data/datasource/RemotePokitDataSourceTest.kt new file mode 100644 index 00000000..3bd15f56 --- /dev/null +++ b/data/src/test/java/pokitmons/pokit/data/datasource/RemotePokitDataSourceTest.kt @@ -0,0 +1,156 @@ +package pokitmons.pokit.data.datasource + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.mockk.coEvery +import io.mockk.mockk +import pokitmons.pokit.data.api.PokitApi +import pokitmons.pokit.data.datasource.remote.pokit.RemotePokitDataSource +import pokitmons.pokit.data.model.pokit.request.CreatePokitRequest +import pokitmons.pokit.data.model.pokit.request.GetPokitsRequest +import pokitmons.pokit.data.model.pokit.request.ModifyPokitRequest +import pokitmons.pokit.data.model.pokit.response.CreatePokitResponse +import pokitmons.pokit.data.model.pokit.response.GetPokitCountResponse +import pokitmons.pokit.data.model.pokit.response.GetPokitImagesResponseItem +import pokitmons.pokit.data.model.pokit.response.GetPokitResponse +import pokitmons.pokit.data.model.pokit.response.GetPokitsResponse +import pokitmons.pokit.data.model.pokit.response.ModifyPokitResponse + +val pokitApi: PokitApi = mockk() + +class RemotePokitDataSourceTest : DescribeSpec({ + val remotePokitDataSource = RemotePokitDataSource(pokitApi) + describe("포킷 목록 조회시") { + context("포킷 목록이 정상적으로 수신되면") { + coEvery { pokitApi.getPokits() } returns GetPokitsResponse() + it("포킷 목록이 반환된다.") { + val response = remotePokitDataSource.getPokits(GetPokitsRequest()) + response.shouldBeInstanceOf() + } + } + + context("에러가 발생했다면") { + coEvery { pokitApi.getPokits() } throws IllegalArgumentException("error") + it("동일한 에러가 발생한다.") { + val exception = shouldThrow { + remotePokitDataSource.getPokits(GetPokitsRequest()) + } + exception.message shouldBe "error" + } + } + } + + describe("포킷 추가시") { + context("추가가 성공적으로 수행되면") { + coEvery { pokitApi.createPokit(CreatePokitRequest()) } returns CreatePokitResponse() + it("추가된 포킷의 정보가 반환된다.") { + val response = remotePokitDataSource.createPokit(CreatePokitRequest()) + response.shouldBeInstanceOf() + } + } + + context("추가도중 에러가 발생했다면") { + coEvery { pokitApi.createPokit(CreatePokitRequest()) } throws IllegalArgumentException("error") + it("동일한 에러가 발생한다.") { + val exception = shouldThrow { + remotePokitDataSource.createPokit(CreatePokitRequest()) + } + exception.message shouldBe "error" + } + } + } + + describe("포킷 수정시") { + context("수정이 성공적으로 수행되면") { + coEvery { pokitApi.modifyPokit(categoryId = 0, modifyPokitRequest = ModifyPokitRequest()) } returns ModifyPokitResponse() + it("수정된 포킷의 정보가 반환된다.") { + val response = remotePokitDataSource.modifyPokit(pokitId = 0, modifyPokitRequest = ModifyPokitRequest()) + response.shouldBeInstanceOf() + } + } + + context("수정도중 에러가 발생했다면") { + coEvery { pokitApi.modifyPokit(categoryId = 0, modifyPokitRequest = ModifyPokitRequest()) } throws IllegalArgumentException("error") + it("동일한 에러가 발생한다.") { + val exception = shouldThrow { + remotePokitDataSource.modifyPokit(pokitId = 0, modifyPokitRequest = ModifyPokitRequest()) + } + exception.message shouldBe "error" + } + } + } + + describe("포킷 조회시") { + context("조회가 성공적으로 수행되면") { + coEvery { pokitApi.getPokit(0) } returns GetPokitResponse() + it("해당 포킷의 정보가 반환된다.") { + val response = remotePokitDataSource.getPokit(0) + response.shouldBeInstanceOf() + } + } + + context("조회 도중 에러가 발생했다면") { + coEvery { pokitApi.getPokit(0) } throws IllegalArgumentException("error") + it("동일한 에러가 발생한다.") { + val exception = shouldThrow { + remotePokitDataSource.getPokit(0) + } + exception.message shouldBe "error" + } + } + } + + describe("포킷 이미지 목록 조회시") { + context("목록 조회가 성공적으로 수행되면") { + coEvery { pokitApi.getPokitImages() } returns emptyList() + it("해당 리스트가 반환된다.") { + val response = remotePokitDataSource.getPokitImages() + response.shouldBeInstanceOf>() + } + } + + context("조회 도중 에러가 발생했다면") { + coEvery { pokitApi.getPokitImages() } throws IllegalArgumentException("error") + it("동일한 에러가 발생한다.") { + val exception = shouldThrow { + remotePokitDataSource.getPokitImages() + } + exception.message shouldBe "error" + } + } + } + + describe("포킷 삭제시") { + context("삭제가 실패한다면") { + coEvery { pokitApi.deletePokit(0) } throws IllegalArgumentException("error") + it("동일한 에러가 발생한다.") { + val exception = shouldThrow { + remotePokitDataSource.deletePokit(0) + } + exception.message shouldBe "error" + } + } + } + + describe("포킷 개수 조회시") { + context("개수 조회가 성공적으로 수행되면") { + coEvery { pokitApi.getPokitCount() } returns GetPokitCountResponse() + it("해당 리스트가 반환된다.") { + val response = remotePokitDataSource.getPokitCount() + response.shouldBeInstanceOf() + } + } + + context("개수 조회 도중 실패한다면") { + coEvery { pokitApi.getPokitCount() } throws IllegalArgumentException("error") + it("동일한 에러가 발생한다.") { + val exception = shouldThrow { + remotePokitDataSource.getPokitCount() + } + exception.message shouldBe "error" + } + } + } +}) diff --git a/data/src/test/java/pokitmons/pokit/data/repository/AlertRepositoryImplTest.kt b/data/src/test/java/pokitmons/pokit/data/repository/AlertRepositoryImplTest.kt new file mode 100644 index 00000000..10fadefc --- /dev/null +++ b/data/src/test/java/pokitmons/pokit/data/repository/AlertRepositoryImplTest.kt @@ -0,0 +1,52 @@ +package pokitmons.pokit.data.repository + +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.types.shouldBeInstanceOf +import io.mockk.coEvery +import io.mockk.mockk +import pokitmons.pokit.data.datasource.remote.alert.AlertDataSource +import pokitmons.pokit.data.model.alert.GetAlertsResponse +import pokitmons.pokit.data.repository.alert.AlertRepositoryImpl +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.alert.Alarm + +val alertDataSource: AlertDataSource = mockk() + +class AlertRepositoryImplTest : DescribeSpec({ + val alertRepository = AlertRepositoryImpl(alertDataSource) + describe("알림 목록 조회시") { + context("알림 목록 조회가 정상적으로 수행되면") { + coEvery { alertDataSource.getAlerts(page = 0, size = 0) } returns GetAlertsResponse() + it("PokitResult로 래핑된 알림 목록이 반환된다.") { + val response = alertRepository.getAlerts(page = 0, size = 0) + response.shouldBeInstanceOf>>() + } + } + + context("에러가 발생했다면") { + coEvery { alertDataSource.getAlerts(page = 0, size = 0) } throws IllegalArgumentException() + it("PokitResult로 래핑된 에러 내용이 반환된다.") { + val response = alertRepository.getAlerts(page = 0, size = 0) + response.shouldBeInstanceOf() + } + } + } + + describe("알림 제거시") { + context("제거가 정상적으로 수행되면") { + coEvery { alertDataSource.deleteAlert(alertId = 0) } returns Unit + it("데이터가 없는 PokitResult.Success가 반환된다.") { + val response = alertRepository.deleteAlert(alertId = 0) + response.shouldBeInstanceOf>() + } + } + + context("에러가 발생했다면") { + coEvery { alertDataSource.deleteAlert(alertId = 0) } throws IllegalArgumentException() + it("PokitResult로 래핑된 에러 내용이 반환된다.") { + val response = alertRepository.deleteAlert(alertId = 0) + response.shouldBeInstanceOf() + } + } + } +}) diff --git a/data/src/test/java/pokitmons/pokit/data/repository/LinkRepositoryImplTest.kt b/data/src/test/java/pokitmons/pokit/data/repository/LinkRepositoryImplTest.kt new file mode 100644 index 00000000..9cee6ade --- /dev/null +++ b/data/src/test/java/pokitmons/pokit/data/repository/LinkRepositoryImplTest.kt @@ -0,0 +1,68 @@ +package pokitmons.pokit.data.repository + +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.types.shouldBeInstanceOf +import io.mockk.coEvery +import io.mockk.mockk +import pokitmons.pokit.data.datasource.remote.link.LinkDataSource +import pokitmons.pokit.data.model.link.response.GetLinksResponse +import pokitmons.pokit.data.repository.link.LinkRepositoryImpl +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.link.Link + +val linkDataSource: LinkDataSource = mockk() + +class LinkRepositoryImplTest : DescribeSpec({ + val linkRepository = LinkRepositoryImpl(linkDataSource) + describe("링크 목록 조회") { + context("링크 목록이 정상적으로 수신되면") { + coEvery { linkDataSource.getLinks() } returns GetLinksResponse() + it("링크 목록이 반환된다.") { + val response = linkRepository.getLinks() + response.shouldBeInstanceOf>>() + } + } + + context("에러가 발생했다면") { + coEvery { linkDataSource.getLinks() } throws IllegalArgumentException() + it("에러코드, 메세지가 반환된다.") { + val response = linkRepository.getLinks() + response.shouldBeInstanceOf() + } + } + } + + describe("북마크 변경") { + context("북마크 취소가 정상적으로 수행된다면") { + coEvery { linkDataSource.setBookmark(contentId = 0, bookmarked = false) } returns Unit + it("빈 성공 결과 인스턴스가 반환된다.") { + val response = linkRepository.setBookmark(linkId = 0, bookmarked = false) + response.shouldBeInstanceOf>() + } + } + + context("북마크 취소 도중 예외가 발생했다면") { + coEvery { linkDataSource.setBookmark(contentId = 0, bookmarked = false) } throws IllegalArgumentException() + it("에러코드, 메세지가 반환된다.") { + val response = linkRepository.setBookmark(linkId = 0, bookmarked = false) + response.shouldBeInstanceOf() + } + } + + context("북마크 등록이 정상적으로 수행된다면") { + coEvery { linkDataSource.setBookmark(contentId = 0, bookmarked = true) } returns Unit + it("빈 성공 결과 인스턴스가 반환된다.") { + val response = linkRepository.setBookmark(linkId = 0, bookmarked = true) + response.shouldBeInstanceOf>() + } + } + + context("북마크 등록 도중 예외가 발생했다면") { + coEvery { linkDataSource.setBookmark(contentId = 0, bookmarked = true) } throws IllegalArgumentException() + it("에러코드, 메세지가 반환된다.") { + val response = linkRepository.setBookmark(linkId = 0, bookmarked = true) + response.shouldBeInstanceOf() + } + } + } +}) diff --git a/data/src/test/java/pokitmons/pokit/data/repository/PokitRepositoryImplTest.kt b/data/src/test/java/pokitmons/pokit/data/repository/PokitRepositoryImplTest.kt new file mode 100644 index 00000000..6c7007ec --- /dev/null +++ b/data/src/test/java/pokitmons/pokit/data/repository/PokitRepositoryImplTest.kt @@ -0,0 +1,151 @@ +package pokitmons.pokit.data.repository + +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.types.shouldBeInstanceOf +import io.mockk.coEvery +import io.mockk.mockk +import pokitmons.pokit.data.datasource.remote.pokit.PokitDataSource +import pokitmons.pokit.data.model.pokit.request.CreatePokitRequest +import pokitmons.pokit.data.model.pokit.request.GetPokitsRequest +import pokitmons.pokit.data.model.pokit.request.ModifyPokitRequest +import pokitmons.pokit.data.model.pokit.response.CreatePokitResponse +import pokitmons.pokit.data.model.pokit.response.GetPokitCountResponse +import pokitmons.pokit.data.model.pokit.response.GetPokitResponse +import pokitmons.pokit.data.model.pokit.response.GetPokitsResponse +import pokitmons.pokit.data.model.pokit.response.ModifyPokitResponse +import pokitmons.pokit.data.repository.pokit.PokitRepositoryImpl +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.pokit.Pokit + +val pokitDataSource: PokitDataSource = mockk() + +class PokitRepositoryImplTest : DescribeSpec({ + val pokitRepository = PokitRepositoryImpl(pokitDataSource) + describe("포킷 목록 조회") { + context("포킷 목록이 정상적으로 수신되면") { + coEvery { pokitDataSource.getPokits(GetPokitsRequest()) } returns GetPokitsResponse() + + it("포킷 목록이 반환된다.") { + val response = pokitRepository.getPokits() + response.shouldBeInstanceOf>>() + } + } + + context("에러가 발생했다면") { + coEvery { pokitDataSource.getPokits(GetPokitsRequest()) } throws IllegalArgumentException() + + it("에러 코드, 메세지가 반환된다.") { + val response = pokitRepository.getPokits() + response.shouldBeInstanceOf() + } + } + } + + describe("포킷 추가시") { + context("추가가 성공적으로 수행되면") { + coEvery { pokitDataSource.createPokit(CreatePokitRequest()) } returns CreatePokitResponse() + it("추가된 포킷의 id가 반환된다.") { + val response = pokitRepository.createPokit("", 0) + response.shouldBeInstanceOf>() + } + } + + context("추가도중 에러가 발생했다면") { + coEvery { pokitDataSource.createPokit(CreatePokitRequest()) } throws IllegalArgumentException() + it("에러 코드, 메세지가 반환된다.") { + val response = pokitRepository.createPokit("", 0) + response.shouldBeInstanceOf() + } + } + } + + describe("포킷 수정시") { + context("수정이 성공적으로 수행되면") { + coEvery { pokitDataSource.modifyPokit(pokitId = 0, modifyPokitRequest = ModifyPokitRequest()) } returns ModifyPokitResponse() + it("수정된 포킷의 id가 반환된다.") { + val response = pokitRepository.modifyPokit(pokitId = 0, name = "", imageId = 0) + response.shouldBeInstanceOf>() + } + } + + context("수정도중 에러가 발생했다면") { + coEvery { pokitDataSource.modifyPokit(pokitId = 0, modifyPokitRequest = ModifyPokitRequest()) } throws IllegalArgumentException() + it("에러 코드, 메세지가 반환된다.") { + val response = pokitRepository.modifyPokit(pokitId = 0, name = "", imageId = 0) + response.shouldBeInstanceOf() + } + } + } + + describe("포킷 조회시") { + context("조회가 성공적으로 수행되면") { + coEvery { pokitDataSource.getPokit(pokitId = 0) } returns GetPokitResponse() + it("해당 포킷의 정보가 반환된다.") { + val response = pokitRepository.getPokit(pokitId = 0) + response.shouldBeInstanceOf>() + } + } + + context("조회 도중 에러가 발생했다면") { + coEvery { pokitDataSource.getPokit(pokitId = 0) } throws IllegalArgumentException() + it("에러 코드, 메세지가 반환된다.") { + val response = pokitRepository.getPokit(pokitId = 0) + response.shouldBeInstanceOf() + } + } + } + + describe("포킷 이미지 목록 조회시") { + context("목록 조회가 성공적으로 수행되면") { + coEvery { pokitDataSource.getPokitImages() } returns emptyList() + it("해당 리스트가 반환된다.") { + val response = pokitRepository.getPokitImages() + response.shouldBeInstanceOf>>() + } + } + + context("조회 도중 에러가 발생한다면") { + coEvery { pokitDataSource.getPokitImages() } throws IllegalArgumentException() + it("에러 코드, 메세지가 반환된다.") { + val response = pokitRepository.getPokitImages() + response.shouldBeInstanceOf() + } + } + } + + describe("포킷 삭제시") { + context("삭제가 성공적으로 수행되면") { + coEvery { pokitDataSource.deletePokit(pokitId = 0) } returns Unit + it("해당 포킷의 정보가 반환된다.") { + val response = pokitRepository.deletePokit(pokitId = 0) + response.shouldBeInstanceOf>() + } + } + + context("삭제 도중 에러가 발생했다면") { + coEvery { pokitDataSource.deletePokit(pokitId = 0) } throws IllegalArgumentException() + it("에러 코드, 메세지가 반환된다.") { + val response = pokitRepository.deletePokit(pokitId = 0) + response.shouldBeInstanceOf() + } + } + } + + describe("포킷 개수 조회시") { + context("개수 조회가 성공적으로 수행되면") { + coEvery { pokitDataSource.getPokitCount() } returns GetPokitCountResponse() + it("개수가 반환된다.") { + val response = pokitRepository.getPokitCount() + response.shouldBeInstanceOf>() + } + } + + context("개수 조회 도중 에러가 발생한다면") { + coEvery { pokitDataSource.getPokitCount() } throws IllegalArgumentException() + it("에러 코드, 메세지가 반환된다.") { + val response = pokitRepository.getPokitCount() + response.shouldBeInstanceOf() + } + } + } +}) diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index 9f63a980..05310eff 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -10,4 +10,5 @@ java { dependencies { implementation(libs.javax.inject) + implementation(libs.coroutines.core) } diff --git a/domain/src/main/java/pokitmons/pokit/domain/model/alert/Alarm.kt b/domain/src/main/java/pokitmons/pokit/domain/model/alert/Alarm.kt new file mode 100644 index 00000000..e9d6164f --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/model/alert/Alarm.kt @@ -0,0 +1,9 @@ +package pokitmons.pokit.domain.model.alert + +data class Alarm( + val id: Int, + val contentId: Int, + val thumbnail: String? = null, + val title: String, + val createdAt: String, +) diff --git a/domain/src/main/java/pokitmons/pokit/domain/model/auth/SignUpResult.kt b/domain/src/main/java/pokitmons/pokit/domain/model/auth/SignUpResult.kt new file mode 100644 index 00000000..8abbc165 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/model/auth/SignUpResult.kt @@ -0,0 +1,7 @@ +package pokitmons.pokit.domain.model.auth + +data class SignUpResult( + val id: Int, + val email: String, + val nickname: String, +) diff --git a/domain/src/main/java/pokitmons/pokit/domain/model/home/remind/RemindResult.kt b/domain/src/main/java/pokitmons/pokit/domain/model/home/remind/RemindResult.kt new file mode 100644 index 00000000..6518a021 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/model/home/remind/RemindResult.kt @@ -0,0 +1,10 @@ +package pokitmons.pokit.domain.model.home.remind + +data class RemindResult( + val title: String, + val domain: String, + val createdAt: String, + val isRead: Boolean, + val thumbNail: String, + val data: String, +) diff --git a/domain/src/main/java/pokitmons/pokit/domain/model/link/Link.kt b/domain/src/main/java/pokitmons/pokit/domain/model/link/Link.kt new file mode 100644 index 00000000..c4933dd0 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/model/link/Link.kt @@ -0,0 +1,16 @@ +package pokitmons.pokit.domain.model.link + +data class Link( + val id: Int, + val categoryId: Int, + val categoryName: String, + val data: String, + val domain: String, + val title: String, + val memo: String, + val alertYn: String, + val createdAt: String, + val isRead: Boolean = false, + val favorites: Boolean = false, + val thumbnail: String = "", +) diff --git a/domain/src/main/java/pokitmons/pokit/domain/model/link/LinkCard.kt b/domain/src/main/java/pokitmons/pokit/domain/model/link/LinkCard.kt new file mode 100644 index 00000000..29b4147d --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/model/link/LinkCard.kt @@ -0,0 +1,7 @@ +package pokitmons.pokit.domain.model.link + +data class LinkCard( + val url: String = "", + val thumbnailUrl: String? = null, + val title: String = "", +) diff --git a/domain/src/main/java/pokitmons/pokit/domain/model/link/LinksSort.kt b/domain/src/main/java/pokitmons/pokit/domain/model/link/LinksSort.kt new file mode 100644 index 00000000..36499268 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/model/link/LinksSort.kt @@ -0,0 +1,5 @@ +package pokitmons.pokit.domain.model.link + +enum class LinksSort(val value: String) { + RECENT("createdAt,desc"), OLDER("createdAt,asc") +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/model/pokit/Pokit.kt b/domain/src/main/java/pokitmons/pokit/domain/model/pokit/Pokit.kt new file mode 100644 index 00000000..06a376f0 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/model/pokit/Pokit.kt @@ -0,0 +1,15 @@ +package pokitmons.pokit.domain.model.pokit + +data class Pokit( + val categoryId: Int, + val userId: Int, + val name: String, + val image: Image, + val linkCount: Int, + val createdAt: String, +) { + data class Image( + val id: Int, + val url: String, + ) +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/model/pokit/PokitErrorCode.kt b/domain/src/main/java/pokitmons/pokit/domain/model/pokit/PokitErrorCode.kt new file mode 100644 index 00000000..ab6521f2 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/model/pokit/PokitErrorCode.kt @@ -0,0 +1,10 @@ +package pokitmons.pokit.domain.model.pokit + +object PokitErrorCode { + const val ALREADY_USED_POKIT_NAME = "C_001" + const val CANNOT_FOUND_POKIT_INFO = "C_002" + const val UNAVAILABLE_POKIT = "C_003" + const val CANNOT_FOUND_POKIT_IMAGE = "C_004" + const val TOO_MUCH_POKIT = "C_005" + const val CANNOT_FOUND_UNCATEGORY_IMAGE = "C_006" +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/model/pokit/PokitsSort.kt b/domain/src/main/java/pokitmons/pokit/domain/model/pokit/PokitsSort.kt new file mode 100644 index 00000000..afe21f35 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/model/pokit/PokitsSort.kt @@ -0,0 +1,5 @@ +package pokitmons.pokit.domain.model.pokit + +enum class PokitsSort(val value: String) { + RECENT("createdAt,desc"), ALPHABETICAL("name,asc") +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/model/setting/EditNicknameResult.kt b/domain/src/main/java/pokitmons/pokit/domain/model/setting/EditNicknameResult.kt new file mode 100644 index 00000000..09119047 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/model/setting/EditNicknameResult.kt @@ -0,0 +1,7 @@ +package pokitmons.pokit.domain.model.setting + +data class EditNicknameResult( + val email: String, + val id: Int, + val nickname: String, +) diff --git a/domain/src/main/java/pokitmons/pokit/domain/repository/alert/AlertRepository.kt b/domain/src/main/java/pokitmons/pokit/domain/repository/alert/AlertRepository.kt new file mode 100644 index 00000000..5b836d9d --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/repository/alert/AlertRepository.kt @@ -0,0 +1,9 @@ +package pokitmons.pokit.domain.repository.alert + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.alert.Alarm + +interface AlertRepository { + suspend fun getAlerts(page: Int, size: Int): PokitResult> + suspend fun deleteAlert(alertId: Int): PokitResult +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/repository/auth/AuthRepository.kt b/domain/src/main/java/pokitmons/pokit/domain/repository/auth/AuthRepository.kt index f50578d8..99220b23 100644 --- a/domain/src/main/java/pokitmons/pokit/domain/repository/auth/AuthRepository.kt +++ b/domain/src/main/java/pokitmons/pokit/domain/repository/auth/AuthRepository.kt @@ -3,12 +3,10 @@ package pokitmons.pokit.domain.repository.auth import pokitmons.pokit.domain.commom.PokitResult import pokitmons.pokit.domain.model.auth.DuplicateNicknameResult import pokitmons.pokit.domain.model.auth.SNSLoginResult +import pokitmons.pokit.domain.model.auth.SignUpResult interface AuthRepository { - suspend fun snsLogin( - authPlatform: String, - idToken: String, - ): PokitResult - + suspend fun snsLogin(authPlatform: String, idToken: String): PokitResult suspend fun checkDuplicateNickname(nickname: String): PokitResult + suspend fun signUp(nickname: String, categories: List): PokitResult } diff --git a/domain/src/main/java/pokitmons/pokit/domain/repository/home/remind/RemindRepository.kt b/domain/src/main/java/pokitmons/pokit/domain/repository/home/remind/RemindRepository.kt new file mode 100644 index 00000000..d86b89f9 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/repository/home/remind/RemindRepository.kt @@ -0,0 +1,28 @@ +package pokitmons.pokit.domain.repository.home.remind + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.home.remind.RemindResult +import pokitmons.pokit.domain.model.pokit.PokitsSort + +interface RemindRepository { + suspend fun getUnReadContents( + filterUncategorized: Boolean = true, + size: Int = 10, + page: Int = 0, + sort: PokitsSort = PokitsSort.RECENT, + ): PokitResult> + + suspend fun getTodayContents( + filterUncategorized: Boolean = true, + size: Int = 10, + page: Int = 0, + sort: PokitsSort = PokitsSort.RECENT, + ): PokitResult> + + suspend fun getBookmarkContents( + filterUncategorized: Boolean = true, + size: Int = 10, + page: Int = 0, + sort: PokitsSort = PokitsSort.RECENT, + ): PokitResult> +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/repository/link/LinkRepository.kt b/domain/src/main/java/pokitmons/pokit/domain/repository/link/LinkRepository.kt new file mode 100644 index 00000000..7bc829df --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/repository/link/LinkRepository.kt @@ -0,0 +1,65 @@ +package pokitmons.pokit.domain.repository.link + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.link.Link +import pokitmons.pokit.domain.model.link.LinkCard +import pokitmons.pokit.domain.model.link.LinksSort + +interface LinkRepository { + suspend fun getLinks( + categoryId: Int = 0, + size: Int = 10, + page: Int = 0, + sort: LinksSort = LinksSort.RECENT, + isRead: Boolean = false, + favorite: Boolean = false, + startDate: String? = null, + endDate: String? = null, + categoryIds: List? = null, + ): PokitResult> + + suspend fun searchLinks( + page: Int, + size: Int, + sort: List, + isRead: Boolean, + favorites: Boolean, + startDate: String?, + endDate: String?, + categoryIds: List?, + searchWord: String, + ): PokitResult> + + suspend fun deleteLink(linkId: Int): PokitResult + + suspend fun getLink(linkId: Int): PokitResult + + suspend fun modifyLink( + linkId: Int, + data: String, + title: String, + categoryId: Int, + memo: String, + alertYn: String, + thumbNail: String, + ): PokitResult + + suspend fun createLink( + data: String, + title: String, + categoryId: Int, + memo: String, + alertYn: String, + thumbNail: String, + ): PokitResult + + suspend fun setBookmark(linkId: Int, bookmarked: Boolean): PokitResult + + suspend fun getLinkCard(url: String): PokitResult + + suspend fun getUncategorizedLinks( + size: Int = 10, + page: Int = 0, + sort: LinksSort = LinksSort.RECENT, + ): PokitResult> +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/repository/pokit/PokitRepository.kt b/domain/src/main/java/pokitmons/pokit/domain/repository/pokit/PokitRepository.kt new file mode 100644 index 00000000..c684f13f --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/repository/pokit/PokitRepository.kt @@ -0,0 +1,33 @@ +package pokitmons.pokit.domain.repository.pokit + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.pokit.Pokit +import pokitmons.pokit.domain.model.pokit.PokitsSort + +interface PokitRepository { + suspend fun getPokits( + filterUncategorized: Boolean = true, + size: Int = 10, + page: Int = 0, + sort: PokitsSort = PokitsSort.RECENT, + ): PokitResult> + + suspend fun createPokit( + name: String, + imageId: Int, + ): PokitResult + + suspend fun modifyPokit( + pokitId: Int, + name: String, + imageId: Int, + ): PokitResult + + suspend fun getPokitImages(): PokitResult> + + suspend fun getPokit(pokitId: Int): PokitResult + + suspend fun deletePokit(pokitId: Int): PokitResult + + suspend fun getPokitCount(): PokitResult +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/repository/search/SearchRepository.kt b/domain/src/main/java/pokitmons/pokit/domain/repository/search/SearchRepository.kt new file mode 100644 index 00000000..26c7a6e6 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/repository/search/SearchRepository.kt @@ -0,0 +1,12 @@ +package pokitmons.pokit.domain.repository.search + +import kotlinx.coroutines.flow.Flow + +interface SearchRepository { + fun getRecentSearchWords(): Flow> + suspend fun removeSearchWord(word: String) + suspend fun removeAllSearchWords() + suspend fun setUseRecentSearchWord(use: Boolean): Boolean + fun getUseRecentSearchWord(): Flow + suspend fun addRecentSearchWord(word: String) +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/repository/setting/SettingRepository.kt b/domain/src/main/java/pokitmons/pokit/domain/repository/setting/SettingRepository.kt new file mode 100644 index 00000000..6cdd03ac --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/repository/setting/SettingRepository.kt @@ -0,0 +1,8 @@ +package pokitmons.pokit.domain.repository.setting + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.setting.EditNicknameResult + +interface SettingRepository { + suspend fun editNickname(nickname: String): PokitResult +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/alert/DeleteAlertUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/alert/DeleteAlertUseCase.kt new file mode 100644 index 00000000..848bcad8 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/alert/DeleteAlertUseCase.kt @@ -0,0 +1,13 @@ +package pokitmons.pokit.domain.usecase.alert + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.repository.alert.AlertRepository +import javax.inject.Inject + +class DeleteAlertUseCase @Inject constructor( + private val repository: AlertRepository, +) { + suspend fun deleteAlert(alertId: Int): PokitResult { + return repository.deleteAlert(alertId) + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/alert/GetAlertsUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/alert/GetAlertsUseCase.kt new file mode 100644 index 00000000..be03aec2 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/alert/GetAlertsUseCase.kt @@ -0,0 +1,14 @@ +package pokitmons.pokit.domain.usecase.alert + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.alert.Alarm +import pokitmons.pokit.domain.repository.alert.AlertRepository +import javax.inject.Inject + +class GetAlertsUseCase @Inject constructor( + private val repository: AlertRepository, +) { + suspend fun getAlerts(page: Int, size: Int): PokitResult> { + return repository.getAlerts(page = page, size = size) + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/auth/SignUpUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/auth/SignUpUseCase.kt index 029c6c3f..35e35f19 100644 --- a/domain/src/main/java/pokitmons/pokit/domain/usecase/auth/SignUpUseCase.kt +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/auth/SignUpUseCase.kt @@ -1,3 +1,18 @@ package pokitmons.pokit.domain.usecase.auth -class SignUpUseCase +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.auth.SignUpResult +import pokitmons.pokit.domain.repository.auth.AuthRepository +import javax.inject.Inject + +class SignUpUseCase @Inject constructor(private val authRepository: AuthRepository) { + suspend fun signUp( + nickname: String, + categories: List, + ): PokitResult { + return when (val signUpResult = authRepository.signUp(nickname, categories)) { + is PokitResult.Success -> PokitResult.Success(signUpResult.result) + is PokitResult.Error -> PokitResult.Error(signUpResult.error) + } + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/home/remind/BookMarkContentsUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/home/remind/BookMarkContentsUseCase.kt new file mode 100644 index 00000000..4d589dc8 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/home/remind/BookMarkContentsUseCase.kt @@ -0,0 +1,12 @@ +package pokitmons.pokit.domain.usecase.home.remind + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.home.remind.RemindResult +import pokitmons.pokit.domain.repository.home.remind.RemindRepository +import javax.inject.Inject + +class BookMarkContentsUseCase @Inject constructor(private val remindRepository: RemindRepository) { + suspend fun getBookmarkContents(): PokitResult> { + return remindRepository.getBookmarkContents() + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/home/remind/TodayContentsUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/home/remind/TodayContentsUseCase.kt new file mode 100644 index 00000000..6a4cf317 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/home/remind/TodayContentsUseCase.kt @@ -0,0 +1,12 @@ +package pokitmons.pokit.domain.usecase.home.remind + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.home.remind.RemindResult +import pokitmons.pokit.domain.repository.home.remind.RemindRepository +import javax.inject.Inject + +class TodayContentsUseCase @Inject constructor(private val remindRepository: RemindRepository) { + suspend fun getTodayContents(): PokitResult> { + return remindRepository.getTodayContents() + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/home/remind/UnReadContentsUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/home/remind/UnReadContentsUseCase.kt new file mode 100644 index 00000000..5b449479 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/home/remind/UnReadContentsUseCase.kt @@ -0,0 +1,12 @@ +package pokitmons.pokit.domain.usecase.home.remind + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.home.remind.RemindResult +import pokitmons.pokit.domain.repository.home.remind.RemindRepository +import javax.inject.Inject + +class UnReadContentsUseCase @Inject constructor(private val remindRepository: RemindRepository) { + suspend fun getUnreadContents(): PokitResult> { + return remindRepository.getUnReadContents() + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/link/CreateLinkUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/link/CreateLinkUseCase.kt new file mode 100644 index 00000000..9c934e1a --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/link/CreateLinkUseCase.kt @@ -0,0 +1,27 @@ +package pokitmons.pokit.domain.usecase.link + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.repository.link.LinkRepository +import javax.inject.Inject + +class CreateLinkUseCase @Inject constructor( + private val repository: LinkRepository, +) { + suspend fun createLink( + data: String, + title: String, + categoryId: Int, + memo: String, + alertYn: String, + thumbNail: String, + ): PokitResult { + return repository.createLink( + data = data, + title = title, + categoryId = categoryId, + memo = memo, + alertYn = alertYn, + thumbNail = thumbNail + ) + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/link/DeleteLinkUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/link/DeleteLinkUseCase.kt new file mode 100644 index 00000000..ac7d6a64 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/link/DeleteLinkUseCase.kt @@ -0,0 +1,13 @@ +package pokitmons.pokit.domain.usecase.link + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.repository.link.LinkRepository +import javax.inject.Inject + +class DeleteLinkUseCase @Inject constructor( + private val repository: LinkRepository, +) { + suspend fun deleteLink(linkId: Int): PokitResult { + return repository.deleteLink(linkId) + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/link/GetLinkCardUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/link/GetLinkCardUseCase.kt new file mode 100644 index 00000000..10f820ec --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/link/GetLinkCardUseCase.kt @@ -0,0 +1,14 @@ +package pokitmons.pokit.domain.usecase.link + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.link.LinkCard +import pokitmons.pokit.domain.repository.link.LinkRepository +import javax.inject.Inject + +class GetLinkCardUseCase @Inject constructor( + private val repository: LinkRepository, +) { + suspend fun getLinkCard(url: String): PokitResult { + return repository.getLinkCard(url = url) + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/link/GetLinkUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/link/GetLinkUseCase.kt new file mode 100644 index 00000000..a9fdc869 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/link/GetLinkUseCase.kt @@ -0,0 +1,14 @@ +package pokitmons.pokit.domain.usecase.link + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.link.Link +import pokitmons.pokit.domain.repository.link.LinkRepository +import javax.inject.Inject + +class GetLinkUseCase @Inject constructor( + private val repository: LinkRepository, +) { + suspend fun getLink(linkId: Int): PokitResult { + return repository.getLink(linkId) + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/link/GetLinksUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/link/GetLinksUseCase.kt new file mode 100644 index 00000000..04bfb2ad --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/link/GetLinksUseCase.kt @@ -0,0 +1,47 @@ +package pokitmons.pokit.domain.usecase.link + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.link.Link +import pokitmons.pokit.domain.model.link.LinksSort +import pokitmons.pokit.domain.repository.link.LinkRepository +import javax.inject.Inject + +class GetLinksUseCase @Inject constructor( + private val repository: LinkRepository, +) { + suspend fun getLinks( + categoryId: Int = 0, + size: Int = 10, + page: Int = 0, + sort: LinksSort = LinksSort.RECENT, + isRead: Boolean = false, + favorite: Boolean = false, + startDate: String? = null, + endDate: String? = null, + categoryIds: List? = null, + ): PokitResult> { + return repository.getLinks( + categoryId = categoryId, + size = size, + page = page, + sort = sort, + isRead = isRead, + favorite = favorite, + startDate = startDate, + endDate = endDate, + categoryIds = categoryIds + ) + } + + suspend fun getUncategorizedLinks( + size: Int = 10, + page: Int = 0, + sort: LinksSort = LinksSort.RECENT, + ): PokitResult> { + return repository.getUncategorizedLinks( + size = size, + page = page, + sort = sort + ) + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/link/ModifyLinkUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/link/ModifyLinkUseCase.kt new file mode 100644 index 00000000..cbe1ba72 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/link/ModifyLinkUseCase.kt @@ -0,0 +1,29 @@ +package pokitmons.pokit.domain.usecase.link + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.repository.link.LinkRepository +import javax.inject.Inject + +class ModifyLinkUseCase @Inject constructor( + private val repository: LinkRepository, +) { + suspend fun modifyLink( + linkId: Int, + data: String, + title: String, + categoryId: Int, + memo: String, + alertYn: String, + thumbNail: String, + ): PokitResult { + return repository.modifyLink( + linkId = linkId, + data = data, + title = title, + categoryId = categoryId, + memo = memo, + alertYn = alertYn, + thumbNail = thumbNail + ) + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/link/SearchLinksUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/link/SearchLinksUseCase.kt new file mode 100644 index 00000000..3e213ebb --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/link/SearchLinksUseCase.kt @@ -0,0 +1,34 @@ +package pokitmons.pokit.domain.usecase.link + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.link.Link +import pokitmons.pokit.domain.repository.link.LinkRepository +import javax.inject.Inject + +class SearchLinksUseCase @Inject constructor( + private val repository: LinkRepository, +) { + suspend fun searchLinks( + page: Int, + size: Int, + sort: List, + isRead: Boolean, + favorites: Boolean, + startDate: String?, + endDate: String?, + categoryIds: List?, + searchWord: String, + ): PokitResult> { + return repository.searchLinks( + page = page, + size = size, + sort = sort, + isRead = isRead, + favorites = favorites, + startDate = startDate, + endDate = endDate, + categoryIds = categoryIds, + searchWord = searchWord + ) + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/link/SetBookmarkUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/link/SetBookmarkUseCase.kt new file mode 100644 index 00000000..c7e9cee3 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/link/SetBookmarkUseCase.kt @@ -0,0 +1,13 @@ +package pokitmons.pokit.domain.usecase.link + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.repository.link.LinkRepository +import javax.inject.Inject + +class SetBookmarkUseCase @Inject constructor( + private val repository: LinkRepository, +) { + suspend fun setBookMarked(linkId: Int, bookmarked: Boolean): PokitResult { + return repository.setBookmark(linkId = linkId, bookmarked = bookmarked) + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/pokit/CreatePokitUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/pokit/CreatePokitUseCase.kt new file mode 100644 index 00000000..9e1df4c2 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/pokit/CreatePokitUseCase.kt @@ -0,0 +1,16 @@ +package pokitmons.pokit.domain.usecase.pokit + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.repository.pokit.PokitRepository +import javax.inject.Inject + +class CreatePokitUseCase @Inject constructor( + private val repository: PokitRepository, +) { + suspend fun createPokit( + name: String, + imageId: Int, + ): PokitResult { + return repository.createPokit(name = name, imageId = imageId) + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/pokit/DeletePokitUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/pokit/DeletePokitUseCase.kt new file mode 100644 index 00000000..b17ffc59 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/pokit/DeletePokitUseCase.kt @@ -0,0 +1,13 @@ +package pokitmons.pokit.domain.usecase.pokit + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.repository.pokit.PokitRepository +import javax.inject.Inject + +class DeletePokitUseCase @Inject constructor( + private val repository: PokitRepository, +) { + suspend fun deletePokit(pokitId: Int): PokitResult { + return repository.deletePokit(pokitId) + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/pokit/GetPokitCountUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/pokit/GetPokitCountUseCase.kt new file mode 100644 index 00000000..8b0c6802 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/pokit/GetPokitCountUseCase.kt @@ -0,0 +1,13 @@ +package pokitmons.pokit.domain.usecase.pokit + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.repository.pokit.PokitRepository +import javax.inject.Inject + +class GetPokitCountUseCase @Inject constructor( + private val repository: PokitRepository, +) { + suspend fun getPokitCount(): PokitResult { + return repository.getPokitCount() + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/pokit/GetPokitImagesUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/pokit/GetPokitImagesUseCase.kt new file mode 100644 index 00000000..c0a49abb --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/pokit/GetPokitImagesUseCase.kt @@ -0,0 +1,14 @@ +package pokitmons.pokit.domain.usecase.pokit + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.pokit.Pokit +import pokitmons.pokit.domain.repository.pokit.PokitRepository +import javax.inject.Inject + +class GetPokitImagesUseCase @Inject constructor( + private val repository: PokitRepository, +) { + suspend fun getImages(): PokitResult> { + return repository.getPokitImages() + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/pokit/GetPokitUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/pokit/GetPokitUseCase.kt new file mode 100644 index 00000000..7134283b --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/pokit/GetPokitUseCase.kt @@ -0,0 +1,14 @@ +package pokitmons.pokit.domain.usecase.pokit + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.pokit.Pokit +import pokitmons.pokit.domain.repository.pokit.PokitRepository +import javax.inject.Inject + +class GetPokitUseCase @Inject constructor( + private val repository: PokitRepository, +) { + suspend fun getPokit(pokitId: Int): PokitResult { + return repository.getPokit(pokitId) + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/pokit/GetPokitsUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/pokit/GetPokitsUseCase.kt new file mode 100644 index 00000000..64a8aeaa --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/pokit/GetPokitsUseCase.kt @@ -0,0 +1,25 @@ +package pokitmons.pokit.domain.usecase.pokit + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.pokit.Pokit +import pokitmons.pokit.domain.model.pokit.PokitsSort +import pokitmons.pokit.domain.repository.pokit.PokitRepository +import javax.inject.Inject + +class GetPokitsUseCase @Inject constructor( + private val repository: PokitRepository, +) { + suspend fun getPokits( + filterUncategorized: Boolean = true, + size: Int = 10, + page: Int = 0, + sort: PokitsSort = PokitsSort.RECENT, + ): PokitResult> { + return repository.getPokits( + filterUncategorized = filterUncategorized, + size = size, + page = page, + sort = sort + ) + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/pokit/ModifyPokitUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/pokit/ModifyPokitUseCase.kt new file mode 100644 index 00000000..13ced61a --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/pokit/ModifyPokitUseCase.kt @@ -0,0 +1,17 @@ +package pokitmons.pokit.domain.usecase.pokit + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.repository.pokit.PokitRepository +import javax.inject.Inject + +class ModifyPokitUseCase @Inject constructor( + private val repository: PokitRepository, +) { + suspend fun modifyPokit( + pokitId: Int, + name: String, + imageId: Int, + ): PokitResult { + return repository.modifyPokit(pokitId = pokitId, name = name, imageId = imageId) + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/search/AddRecentSearchWordUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/search/AddRecentSearchWordUseCase.kt new file mode 100644 index 00000000..c63a11a4 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/search/AddRecentSearchWordUseCase.kt @@ -0,0 +1,12 @@ +package pokitmons.pokit.domain.usecase.search + +import pokitmons.pokit.domain.repository.search.SearchRepository +import javax.inject.Inject + +class AddRecentSearchWordUseCase @Inject constructor( + private val repository: SearchRepository, +) { + suspend fun addRecentSearchWord(word: String) { + repository.addRecentSearchWord(word) + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/search/GetRecentSearchWordsUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/search/GetRecentSearchWordsUseCase.kt new file mode 100644 index 00000000..b7034c48 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/search/GetRecentSearchWordsUseCase.kt @@ -0,0 +1,13 @@ +package pokitmons.pokit.domain.usecase.search + +import kotlinx.coroutines.flow.Flow +import pokitmons.pokit.domain.repository.search.SearchRepository +import javax.inject.Inject + +class GetRecentSearchWordsUseCase @Inject constructor( + private val repository: SearchRepository, +) { + fun getWords(): Flow> { + return repository.getRecentSearchWords() + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/search/GetUseRecentSearchWordsUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/search/GetUseRecentSearchWordsUseCase.kt new file mode 100644 index 00000000..da55a3fc --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/search/GetUseRecentSearchWordsUseCase.kt @@ -0,0 +1,13 @@ +package pokitmons.pokit.domain.usecase.search + +import kotlinx.coroutines.flow.Flow +import pokitmons.pokit.domain.repository.search.SearchRepository +import javax.inject.Inject + +class GetUseRecentSearchWordsUseCase @Inject constructor( + private val repository: SearchRepository, +) { + fun getUse(): Flow { + return repository.getUseRecentSearchWord() + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/search/RemoveRecentSearchWordUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/search/RemoveRecentSearchWordUseCase.kt new file mode 100644 index 00000000..1745d408 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/search/RemoveRecentSearchWordUseCase.kt @@ -0,0 +1,16 @@ +package pokitmons.pokit.domain.usecase.search + +import pokitmons.pokit.domain.repository.search.SearchRepository +import javax.inject.Inject + +class RemoveRecentSearchWordUseCase @Inject constructor( + private val repository: SearchRepository, +) { + suspend fun removeWord(word: String) { + repository.removeSearchWord(word) + } + + suspend fun removeAll() { + repository.removeAllSearchWords() + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/search/SetUseRecentSearchWordsUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/search/SetUseRecentSearchWordsUseCase.kt new file mode 100644 index 00000000..25c7983e --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/search/SetUseRecentSearchWordsUseCase.kt @@ -0,0 +1,12 @@ +package pokitmons.pokit.domain.usecase.search + +import pokitmons.pokit.domain.repository.search.SearchRepository +import javax.inject.Inject + +class SetUseRecentSearchWordsUseCase @Inject constructor( + private val repository: SearchRepository, +) { + suspend fun setUse(use: Boolean): Boolean { + return repository.setUseRecentSearchWord(use) + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/setting/EditNicknameUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/setting/EditNicknameUseCase.kt new file mode 100644 index 00000000..b439e285 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/setting/EditNicknameUseCase.kt @@ -0,0 +1,15 @@ +package pokitmons.pokit.domain.usecase.setting + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.setting.EditNicknameResult +import pokitmons.pokit.domain.repository.setting.SettingRepository +import javax.inject.Inject + +class EditNicknameUseCase @Inject constructor(private val settingRepository: SettingRepository) { + suspend fun editNickname(nickname: String): PokitResult { + return when (val editNicknameResult = settingRepository.editNickname(nickname)) { + is PokitResult.Success -> PokitResult.Success(editNicknameResult.result) + is PokitResult.Error -> PokitResult.Error(editNicknameResult.error) + } + } +} diff --git a/feature/addlink/build.gradle.kts b/feature/addlink/build.gradle.kts index d37b8318..ff240027 100644 --- a/feature/addlink/build.gradle.kts +++ b/feature/addlink/build.gradle.kts @@ -1,6 +1,8 @@ plugins { alias(libs.plugins.com.android.library) alias(libs.plugins.org.jetbrains.kotlin.android) + alias(libs.plugins.hilt) + id("kotlin-kapt") } android { @@ -60,5 +62,13 @@ dependencies { implementation(libs.orbit.core) implementation(libs.orbit.viewmodel) + // coil + implementation(libs.coil.compose) + + // hilt + implementation(libs.hilt) + kapt(libs.hilt.compiler) + implementation(project(":core:ui")) + implementation(project(":domain")) } diff --git a/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkScreen.kt b/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkScreen.kt index 04332847..71dfd3b9 100644 --- a/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkScreen.kt +++ b/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkScreen.kt @@ -3,6 +3,8 @@ package com.strayalpaca.addlink import android.widget.Toast import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.LocalOverscrollConfiguration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -13,6 +15,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text @@ -20,6 +23,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -32,8 +36,8 @@ import com.strayalpaca.addlink.components.block.Link import com.strayalpaca.addlink.components.block.Toolbar import com.strayalpaca.addlink.model.AddLinkScreenSideEffect import com.strayalpaca.addlink.model.AddLinkScreenState -import com.strayalpaca.addlink.model.Pokit import com.strayalpaca.addlink.model.ScreenStep +import com.strayalpaca.addlink.paging.SimplePagingState import com.strayalpaca.addlink.utils.BackPressHandler import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect @@ -53,21 +57,15 @@ import pokitmons.pokit.core.ui.theme.PokitTheme @Composable fun AddLinkScreenContainer( - linkId: String?, viewModel: AddLinkViewModel, onBackPressed: () -> Unit, + onNavigateToAddPokit: () -> Unit, ) { val state by viewModel.collectAsState() val context = LocalContext.current BackPressHandler(onBackPressed = viewModel::onBackPressed) - LaunchedEffect(Unit) { - linkId?.let { - viewModel.loadPokitLink(it) - } - } - viewModel.collectSideEffect { sideEffect -> when (sideEffect) { AddLinkScreenSideEffect.AddLinkSuccess -> { @@ -87,25 +85,60 @@ fun AddLinkScreenContainer( val url by viewModel.linkUrl.collectAsState() val title by viewModel.title.collectAsState() val memo by viewModel.memo.collectAsState() - val pokitName by viewModel.pokitName.collectAsState() + val pokitList by viewModel.pokitList.collectAsState() + val pokitListState by viewModel.pokitListState.collectAsState() + + PokitBottomSheet( + onHideBottomSheet = viewModel::hideSelectPokitBottomSheet, + show = state.step == ScreenStep.POKIT_SELECT + ) { + val lazyColumnListState = rememberLazyListState() + val startPaging = remember { + derivedStateOf { + lazyColumnListState.layoutInfo.visibleItemsInfo.lastOrNull()?.let { last -> + last.index >= lazyColumnListState.layoutInfo.totalItemsCount - 3 + } ?: false + } + } + + LaunchedEffect(Unit) { + viewModel.refreshPokits() + } + + LaunchedEffect(startPaging.value) { + if (startPaging.value && pokitListState == SimplePagingState.IDLE) { + viewModel.loadNextPokits() + } + } + + LazyColumn( + state = lazyColumnListState + ) { + items( + items = pokitList + ) { pokit -> + PokitList( + item = pokit, + title = pokit.title, + sub = stringResource(id = R.string.count_format, pokit.count), + onClickItem = viewModel::selectPokit, + state = PokitListState.ACTIVE + ) + } + } + } AddLinkScreen( - isModifyLink = (linkId != null), + isModifyLink = (viewModel.currentLinkId != null), url = url, title = title, memo = memo, state = state, - pokitName = pokitName, inputUrl = viewModel::inputLinkUrl, inputTitle = viewModel::inputTitle, inputMemo = viewModel::inputMemo, - inputNewPokitName = viewModel::inputNewPokitName, - onClickAddPokit = viewModel::showAddPokitBottomSheet, - onClickSavePokit = viewModel::savePokit, - dismissPokitAddBottomSheet = viewModel::hideAddPokitBottomSheet, + onClickAddPokit = onNavigateToAddPokit, onClickSelectPokit = viewModel::showSelectPokitBottomSheet, - onClickSelectPokitItem = viewModel::selectPokit, - dismissPokitSelectBottomSheet = viewModel::hideSelectPokitBottomSheet, toggleRemindRadio = viewModel::setRemind, onBackPressed = viewModel::onBackPressed, onClickSaveButton = viewModel::saveLink @@ -119,18 +152,12 @@ fun AddLinkScreen( url: String, title: String, memo: String, - pokitName: String, state: AddLinkScreenState, inputUrl: (String) -> Unit, inputTitle: (String) -> Unit, inputMemo: (String) -> Unit, - inputNewPokitName: (String) -> Unit, onClickAddPokit: () -> Unit, - onClickSavePokit: () -> Unit, - dismissPokitAddBottomSheet: () -> Unit, onClickSelectPokit: () -> Unit, - onClickSelectPokitItem: (Pokit) -> Unit, - dismissPokitSelectBottomSheet: () -> Unit, toggleRemindRadio: (Boolean) -> Unit, onBackPressed: () -> Unit, onClickSaveButton: () -> Unit, @@ -140,14 +167,17 @@ fun AddLinkScreen( !( state.step == ScreenStep.SAVE_LOADING || state.step == ScreenStep.LOADING || - state.step == ScreenStep.POKIT_ADD_LOADING || - state.step == ScreenStep.LINK_LOADING + state.step == ScreenStep.POKIT_ADD_LOADING ) } Column( - modifier = Modifier.fillMaxSize() + modifier = Modifier + .fillMaxSize() + .background(PokitTheme.colors.backgroundBase) ) { + Spacer(modifier = Modifier.height(8.dp)) + Toolbar( modifier = Modifier.fillMaxWidth(), onClickBack = onBackPressed, @@ -159,18 +189,20 @@ fun AddLinkScreen( ) { Column( modifier = Modifier - .padding(vertical = 16.dp, horizontal = 20.dp) + .padding(horizontal = 20.dp) + .weight(1f) .verticalScroll( state = scrollState, flingBehavior = null ) ) { + Spacer(modifier = Modifier.height(16.dp)) + if (state.link != null) { Link(state.link) + Spacer(modifier = Modifier.height(16.dp)) } - Spacer(modifier = Modifier.height(16.dp)) - LabeledInput( label = stringResource(id = R.string.link), sub = "", @@ -283,62 +315,17 @@ fun AddLinkScreen( ) Spacer(modifier = Modifier.height(32.dp)) - - PokitButton( - text = stringResource(id = R.string.save), - icon = null, - onClick = onClickSaveButton, - modifier = Modifier.fillMaxWidth(), - size = PokitButtonSize.LARGE - ) } } - PokitBottomSheet( - onHideBottomSheet = dismissPokitSelectBottomSheet, - show = state.step == ScreenStep.POKIT_SELECT - ) { - LazyColumn { - items( - items = state.pokitList - ) { pokit -> - PokitList( - item = pokit, - title = pokit.title, - sub = stringResource(id = R.string.count_format, pokit.count), - onClickItem = onClickSelectPokitItem, - state = PokitListState.ACTIVE - ) - } - } - } - - PokitBottomSheet( - onHideBottomSheet = dismissPokitAddBottomSheet, - show = state.step == ScreenStep.POKIT_ADD - ) { - Column( - modifier = Modifier.padding(horizontal = 20.dp) - ) { - LabeledInput( - label = "", - inputText = pokitName, - hintText = stringResource(id = R.string.placeholder_input_pokit_name), - onChangeText = inputNewPokitName, - maxLength = 10 - ) - - Spacer(modifier = Modifier.height(12.dp)) - - PokitButton( - text = stringResource(id = R.string.add), - icon = null, - onClick = onClickSavePokit, - modifier = Modifier.fillMaxWidth(), - size = PokitButtonSize.LARGE, - enable = pokitName.isNotEmpty() - ) - } + Box(modifier = Modifier.padding(start = 20.dp, end = 20.dp, bottom = 20.dp)) { + PokitButton( + text = stringResource(id = R.string.save), + icon = null, + onClick = onClickSaveButton, + modifier = Modifier.fillMaxWidth(), + size = PokitButtonSize.LARGE + ) } } } diff --git a/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkViewModel.kt b/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkViewModel.kt index 8f607b45..74d41475 100644 --- a/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkViewModel.kt +++ b/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkViewModel.kt @@ -1,13 +1,17 @@ package com.strayalpaca.addlink +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.strayalpaca.addlink.model.AddLinkScreenSideEffect import com.strayalpaca.addlink.model.AddLinkScreenState +import com.strayalpaca.addlink.model.Link import com.strayalpaca.addlink.model.Pokit import com.strayalpaca.addlink.model.ScreenStep -import com.strayalpaca.addlink.model.sampleLink -import com.strayalpaca.addlink.model.samplePokitList +import com.strayalpaca.addlink.model.ToastMessageEvent +import com.strayalpaca.addlink.paging.PokitPaging +import com.strayalpaca.addlink.paging.SimplePagingState +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -22,10 +26,35 @@ 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 - -class AddLinkViewModel : ContainerHost, ViewModel() { +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.usecase.link.CreateLinkUseCase +import pokitmons.pokit.domain.usecase.link.GetLinkCardUseCase +import pokitmons.pokit.domain.usecase.link.GetLinkUseCase +import pokitmons.pokit.domain.usecase.link.ModifyLinkUseCase +import pokitmons.pokit.domain.usecase.pokit.GetPokitsUseCase +import javax.inject.Inject + +@HiltViewModel +class AddLinkViewModel @Inject constructor( + private val getLinkUseCase: GetLinkUseCase, + private val getLinkCardUseCase: GetLinkCardUseCase, + private val createLinkUseCase: CreateLinkUseCase, + private val modifyLinkUseCase: ModifyLinkUseCase, + getPokitsUseCase: GetPokitsUseCase, + savedStateHandle: SavedStateHandle, +) : ContainerHost, ViewModel() { override val container: Container = container(AddLinkScreenState()) + private val pokitPaging = PokitPaging( + getPokits = getPokitsUseCase, + perPage = 10, + coroutineScope = viewModelScope, + initPage = 0 + ) + + val pokitList: StateFlow> = pokitPaging.pagingData + val pokitListState: StateFlow = pokitPaging.pagingState + private val _linkUrl = MutableStateFlow("") val linkUrl: StateFlow = _linkUrl.asStateFlow() @@ -35,38 +64,36 @@ class AddLinkViewModel : ContainerHost = _memo.asStateFlow() - private val _pokitName = MutableStateFlow("") - val pokitName: StateFlow = _pokitName.asStateFlow() + val currentLinkId: Int? = savedStateHandle.get("link_id")?.toIntOrNull() init { - loadPokitList() + currentLinkId?.let { linkId -> + loadPokitLink(linkId) + } } private var inputLinkJob: Job? = null - private fun loadPokitList() = intent { + private fun loadPokitLink(linkId: Int) = intent { viewModelScope.launch(Dispatchers.IO) { reduce { state.copy(step = ScreenStep.LOADING) } - // todo 포킷 목록 가져오기 api 연결 - delay(1000L) - reduce { - state.copy( - step = ScreenStep.IDLE, - pokitList = samplePokitList - ) - } - } - } - - fun loadPokitLink(linkId: String) = intent { - viewModelScope.launch(Dispatchers.IO) { - reduce { state.copy(step = ScreenStep.LOADING) } - // todo 포킷 링크 가져오기 api 연결 - delay(1000L) - reduce { - state.copy( - step = ScreenStep.IDLE - ) + val response = getLinkUseCase.getLink(linkId) + if (response is PokitResult.Success) { + val responseResult = response.result + reduce { + state.copy( + link = Link.fromDomainLink(responseResult), + useRemind = responseResult.alertYn == "Y", + currentPokit = Pokit( + title = responseResult.categoryName, + id = responseResult.categoryId.toString(), + count = 0 + ), + step = ScreenStep.IDLE + ) + } + } else { + postSideEffect(AddLinkScreenSideEffect.OnNavigationBack) } } } @@ -78,10 +105,14 @@ class AddLinkViewModel : ContainerHost = emptyList(), val useRemind: Boolean = false, val step: ScreenStep = ScreenStep.IDLE, ) diff --git a/feature/addlink/src/main/java/com/strayalpaca/addlink/model/Link.kt b/feature/addlink/src/main/java/com/strayalpaca/addlink/model/Link.kt index b05521d4..361cfcb0 100644 --- a/feature/addlink/src/main/java/com/strayalpaca/addlink/model/Link.kt +++ b/feature/addlink/src/main/java/com/strayalpaca/addlink/model/Link.kt @@ -1,9 +1,28 @@ package com.strayalpaca.addlink.model +import pokitmons.pokit.domain.model.link.Link as DomainLink +import pokitmons.pokit.domain.model.link.LinkCard as DomainLinkCard + data class Link( val url: String, val title: String, val imageUrl: String?, -) +) { + companion object { + fun fromDomainLink(domainLink: DomainLink): Link { + return Link( + url = domainLink.data, + title = domainLink.title, + imageUrl = domainLink.thumbnail + ) + } -internal val sampleLink = Link(url = "https://pokit.com/watch?v=xSTwqkUyM8k", title = "자연 친화적인 라이프스타일을 위한 환경 보호 방법", imageUrl = null) + fun fromDomainLinkCard(domainLinkCard: DomainLinkCard): Link { + return Link( + url = domainLinkCard.url, + title = domainLinkCard.title, + imageUrl = domainLinkCard.thumbnailUrl + ) + } + } +} diff --git a/feature/addlink/src/main/java/com/strayalpaca/addlink/model/Pokit.kt b/feature/addlink/src/main/java/com/strayalpaca/addlink/model/Pokit.kt index 0088afe6..79c6450d 100644 --- a/feature/addlink/src/main/java/com/strayalpaca/addlink/model/Pokit.kt +++ b/feature/addlink/src/main/java/com/strayalpaca/addlink/model/Pokit.kt @@ -1,15 +1,19 @@ package com.strayalpaca.addlink.model +import pokitmons.pokit.domain.model.pokit.Pokit as DomainPokit + data class Pokit( val title: String, val id: String, val count: Int, -) - -internal val samplePokitList = listOf( - Pokit(title = "안드로이드", id = "1", count = 2), - Pokit(title = "IOS", id = "2", count = 2), - Pokit(title = "디자인", id = "3", count = 2), - Pokit(title = "PM", id = "4", count = 1), - Pokit(title = "서버", id = "5", count = 2) -) +) { + companion object { + fun fromDomainPokit(pokit: DomainPokit): Pokit { + return Pokit( + title = pokit.name, + id = pokit.categoryId.toString(), + count = pokit.linkCount + ) + } + } +} diff --git a/feature/addlink/src/main/java/com/strayalpaca/addlink/paging/PokitPaging.kt b/feature/addlink/src/main/java/com/strayalpaca/addlink/paging/PokitPaging.kt new file mode 100644 index 00000000..aeeed146 --- /dev/null +++ b/feature/addlink/src/main/java/com/strayalpaca/addlink/paging/PokitPaging.kt @@ -0,0 +1,124 @@ +package com.strayalpaca.addlink.paging + +import com.strayalpaca.addlink.model.Pokit +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.usecase.pokit.GetPokitsUseCase +import kotlin.coroutines.cancellation.CancellationException + +class PokitPaging( + private val getPokits: GetPokitsUseCase, + private val perPage: Int = 10, + private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO), + private val initPage: Int = 0, + private val firstRequestPage: Int = 3, +) : SimplePaging { + private val _pagingState = MutableStateFlow(SimplePagingState.IDLE) + override val pagingState: StateFlow = _pagingState.asStateFlow() + + private val _pagingData: MutableStateFlow> = MutableStateFlow(emptyList()) + override val pagingData: StateFlow> = _pagingData.asStateFlow() + private var currentPageIndex = initPage + private var requestJob: Job? = null + + override suspend fun refresh() { + requestJob?.cancel() + + _pagingData.update { emptyList() } + _pagingState.update { SimplePagingState.LOADING_INIT } + requestJob = coroutineScope.launch { + try { + currentPageIndex = initPage + val response = getPokits.getPokits(size = perPage * firstRequestPage, page = currentPageIndex) + when (response) { + is PokitResult.Success -> { + val pokitList = response.result.map { domainPokit -> + Pokit.fromDomainPokit(domainPokit) + } + applyResponse(pokitList, firstRequestPage) + } + + is PokitResult.Error -> { + _pagingState.update { SimplePagingState.FAILURE_INIT } + } + } + } catch (exception: Exception) { + if (exception !is CancellationException) { + _pagingState.update { SimplePagingState.FAILURE_INIT } + } + } + } + } + + override suspend fun load() { + if (pagingState.value != SimplePagingState.IDLE) return + + requestJob?.cancel() + _pagingState.update { SimplePagingState.LOADING_NEXT } + + requestJob = coroutineScope.launch { + try { + val response = getPokits.getPokits(size = perPage, page = currentPageIndex) + when (response) { + is PokitResult.Success -> { + val pokitList = response.result.map { domainPokit -> + Pokit.fromDomainPokit(domainPokit) + } + applyResponse(pokitList) + } + + is PokitResult.Error -> { + _pagingState.update { SimplePagingState.FAILURE_NEXT } + } + } + } catch (exception: Exception) { + if (exception !is CancellationException) { + _pagingState.update { SimplePagingState.FAILURE_NEXT } + } + } + } + } + + private fun applyResponse(dataInResponse: List, multiple: Int = 1) { + if (dataInResponse.size < perPage * multiple) { + _pagingState.update { SimplePagingState.LAST } + } else { + _pagingState.update { SimplePagingState.IDLE } + } + _pagingData.update { _pagingData.value + dataInResponse } + currentPageIndex += multiple + } + + override fun clear() { + requestJob?.cancel() + _pagingData.update { emptyList() } + _pagingState.update { SimplePagingState.IDLE } + } + + override suspend fun deleteItem(item: Pokit) { + val capturedDataList = _pagingData.value + _pagingData.update { capturedDataList.filter { it.id != item.id } } + } + + override suspend fun modifyItem(item: Pokit) { + val capturedDataList = _pagingData.value + val targetPokit = capturedDataList.find { it.id == item.id } ?: return + + _pagingData.update { + capturedDataList.map { pokit -> + if (targetPokit.id == pokit.id) { + item + } else { + pokit + } + } + } + } +} diff --git a/feature/addlink/src/main/java/com/strayalpaca/addlink/paging/SimplePaging.kt b/feature/addlink/src/main/java/com/strayalpaca/addlink/paging/SimplePaging.kt new file mode 100644 index 00000000..e89daa6a --- /dev/null +++ b/feature/addlink/src/main/java/com/strayalpaca/addlink/paging/SimplePaging.kt @@ -0,0 +1,13 @@ +package com.strayalpaca.addlink.paging + +import kotlinx.coroutines.flow.Flow + +interface SimplePaging { + val pagingData: Flow> + suspend fun refresh() + suspend fun load() + val pagingState: Flow + suspend fun modifyItem(item: T) + suspend fun deleteItem(item: T) + fun clear() +} diff --git a/feature/addlink/src/main/java/com/strayalpaca/addlink/paging/SimplePagingState.kt b/feature/addlink/src/main/java/com/strayalpaca/addlink/paging/SimplePagingState.kt new file mode 100644 index 00000000..4c1bfc2c --- /dev/null +++ b/feature/addlink/src/main/java/com/strayalpaca/addlink/paging/SimplePagingState.kt @@ -0,0 +1,5 @@ +package com.strayalpaca.addlink.paging + +enum class SimplePagingState { + IDLE, LOADING_NEXT, LOADING_INIT, FAILURE_NEXT, FAILURE_INIT, LAST +} diff --git a/feature/addpokit/build.gradle.kts b/feature/addpokit/build.gradle.kts index 0c700c02..de2cc726 100644 --- a/feature/addpokit/build.gradle.kts +++ b/feature/addpokit/build.gradle.kts @@ -1,6 +1,8 @@ plugins { alias(libs.plugins.com.android.library) alias(libs.plugins.org.jetbrains.kotlin.android) + alias(libs.plugins.hilt) + id("kotlin-kapt") } android { @@ -60,5 +62,13 @@ dependencies { implementation(libs.orbit.core) implementation(libs.orbit.viewmodel) + // hilt + implementation(libs.hilt) + kapt(libs.hilt.compiler) + + // coil + implementation(libs.coil.compose) + implementation(project(":core:ui")) + implementation(project(":domain")) } diff --git a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/AddPokitScreen.kt b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/AddPokitScreen.kt index 34b7e6d1..e9d95822 100644 --- a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/AddPokitScreen.kt +++ b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/AddPokitScreen.kt @@ -20,11 +20,13 @@ import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -33,17 +35,19 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage import com.strayalpaca.addpokit.components.atom.PokitProfileImage import com.strayalpaca.addpokit.components.block.Toolbar import com.strayalpaca.addpokit.model.AddPokitScreenState import com.strayalpaca.addpokit.model.AddPokitScreenStep import com.strayalpaca.addpokit.model.AddPokitSideEffect import com.strayalpaca.addpokit.model.Pokit -import com.strayalpaca.addpokit.model.PokitProfile -import com.strayalpaca.addpokit.model.samplePokitProfileList +import com.strayalpaca.addpokit.model.PokitImage +import com.strayalpaca.addpokit.paging.SimplePagingState import com.strayalpaca.addpokit.utils.BackPressHandler import org.orbitmvi.orbit.compose.collectSideEffect import pokitmons.pokit.core.ui.components.atom.button.PokitButton @@ -60,28 +64,36 @@ import pokitmons.pokit.core.ui.R.string as coreString fun AddPokitScreenContainer( viewModel: AddPokitViewModel, onBackPressed: () -> Unit, + onBackWithModifySuccess: (Int) -> Unit = {}, + onBackWithCreateSuccess: () -> Unit = {}, ) { val state by viewModel.container.stateFlow.collectAsState() val pokitName by viewModel.pokitName.collectAsState() + val images by viewModel.pokitImages.collectAsState() + val pokits by viewModel.pokitList.collectAsState() + val pokitsState by viewModel.pokitListState.collectAsState() val saveButtonEnable = remember { derivedStateOf { state.step != AddPokitScreenStep.POKIT_SAVE_LOADING && - state.step != AddPokitScreenStep.POKIT_LIST_LOADING && state.pokitInputErrorMessage == null && - state.pokitProfile != null + state.pokitImage != null } } viewModel.collectSideEffect { sideEffect -> when (sideEffect) { AddPokitSideEffect.AddPokitSuccess -> { - onBackPressed() + onBackWithCreateSuccess() } AddPokitSideEffect.OnNavigationBack -> { onBackPressed() } + + is AddPokitSideEffect.ModifyPokitSuccess -> { + onBackWithModifySuccess(sideEffect.id) + } } } @@ -96,7 +108,11 @@ fun AddPokitScreenContainer( onBackPressed = viewModel::onBackPressed, hideProfileSelectBottomSheet = viewModel::hidePokitProfileSelectBottomSheet, showSelectProfileBottomSheet = viewModel::showPokitProfileSelectBottomSheet, - selectPokitProfileImage = viewModel::selectPoktiProfile + selectPokitProfileImage = viewModel::selectPokitProfile, + pokits = pokits, + pokitsState = pokitsState, + loadPokits = viewModel::loadPokitList, + pokitImages = images ) } @@ -110,22 +126,32 @@ fun AddPokitScreen( onBackPressed: () -> Unit = {}, hideProfileSelectBottomSheet: () -> Unit = {}, showSelectProfileBottomSheet: () -> Unit = {}, - selectPokitProfileImage: (PokitProfile) -> Unit = {}, + selectPokitProfileImage: (PokitImage) -> Unit = {}, + pokits: List = emptyList(), + pokitsState: SimplePagingState = SimplePagingState.IDLE, + loadPokits: () -> Unit = {}, + pokitImages: List = emptyList(), + pokitImagesState: SimplePagingState = SimplePagingState.IDLE, ) { Column( modifier = Modifier .fillMaxSize() - .padding(vertical = 16.dp), + .background(color = PokitTheme.colors.backgroundBase), horizontalAlignment = Alignment.CenterHorizontally ) { + Spacer(modifier = Modifier.height(8.dp)) + Toolbar( onClickBack = onBackPressed, - title = stringResource(id = R.string.title_add_pokit) + title = stringResource(id = if (state.isModify) R.string.title_modify_pokit else R.string.title_add_pokit) ) + Spacer(modifier = Modifier.height(16.dp)) + Box(modifier = Modifier.size(80.dp)) { - Image( - painter = painterResource(id = coreDrawable.icon_24_google), + AsyncImage( + model = state.pokitImage?.url, + contentScale = ContentScale.Crop, contentDescription = null, modifier = Modifier .size(80.dp) @@ -175,7 +201,7 @@ fun AddPokitScreen( hintText = stringResource(id = R.string.placeholder_pokit_name), onChangeText = inputPokitName, isError = state.pokitInputErrorMessage != null, - sub = state.pokitInputErrorMessage?.let { stringResource(id = it.resourceId) } ?: "", + sub = state.pokitInputErrorMessage ?: "", enable = (state.step != AddPokitScreenStep.POKIT_SAVE_LOADING), maxLength = 10 ) @@ -198,21 +224,37 @@ fun AddPokitScreen( .weight(1f), contentAlignment = Alignment.Center ) { + val pokitLazyColumnListState = rememberLazyListState() + val startPokitPaging = remember { + derivedStateOf { + pokitLazyColumnListState.layoutInfo.visibleItemsInfo.lastOrNull()?.let { last -> + last.index >= pokitLazyColumnListState.layoutInfo.totalItemsCount - 3 + } ?: false + } + } + + LaunchedEffect(startPokitPaging.value) { + if (startPokitPaging.value && pokitsState == SimplePagingState.IDLE) { + loadPokits() + } + } + LazyColumn( - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), + state = pokitLazyColumnListState ) { - items(state.pokitList) { item: Pokit -> + items(pokits) { item: Pokit -> PokitList( item = item, title = item.title, sub = stringResource(id = coreString.pokit_count_format, item.count), onClickItem = {}, - state = PokitListState.DEFAULT + state = PokitListState.DISABLE ) } } - if (state.step == AddPokitScreenStep.POKIT_LIST_LOADING) { + if (pokitImagesState == SimplePagingState.LOADING_INIT) { CircularProgressIndicator( modifier = Modifier.width(64.dp), color = PokitTheme.colors.brand, @@ -224,7 +266,7 @@ fun AddPokitScreen( Box( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 20.dp) + .padding(horizontal = 20.dp, vertical = 16.dp) ) { PokitButton( text = stringResource(id = R.string.save), @@ -241,16 +283,16 @@ fun AddPokitScreen( show = state.step == AddPokitScreenStep.SELECT_PROFILE ) { LazyVerticalGrid( - modifier = Modifier.padding(vertical = 12.dp, horizontal = 40.dp), + modifier = Modifier.padding(vertical = 12.dp, horizontal = 52.dp), columns = GridCells.Adaptive(66.dp), horizontalArrangement = Arrangement.spacedBy(20.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { - items(samplePokitProfileList) { profileImage -> + items(pokitImages) { pokitImage -> PokitProfileImage( - pokitProfile = profileImage, + pokitImage = pokitImage, onClick = selectPokitProfileImage, - focused = (state.pokitProfile?.id == profileImage.id) + focused = (state.pokitImage?.id == pokitImage.id) ) } } diff --git a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/AddPokitViewModel.kt b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/AddPokitViewModel.kt index 590775db..985fa6f6 100644 --- a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/AddPokitViewModel.kt +++ b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/AddPokitViewModel.kt @@ -1,15 +1,18 @@ package com.strayalpaca.addpokit +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.strayalpaca.addpokit.const.POKIT_NAME_MAX_LENGTH import com.strayalpaca.addpokit.model.AddPokitScreenState import com.strayalpaca.addpokit.model.AddPokitScreenStep import com.strayalpaca.addpokit.model.AddPokitSideEffect -import com.strayalpaca.addpokit.model.PokitInputErrorMessage -import com.strayalpaca.addpokit.model.PokitProfile -import com.strayalpaca.addpokit.model.samplePokitList -import kotlinx.coroutines.delay +import com.strayalpaca.addpokit.model.Pokit +import com.strayalpaca.addpokit.model.PokitImage +import com.strayalpaca.addpokit.paging.PokitPaging +import com.strayalpaca.addpokit.paging.SimplePagingState +import com.strayalpaca.addpokit.utils.ErrorMessageProvider +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -21,34 +24,89 @@ 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 pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.usecase.pokit.CreatePokitUseCase +import pokitmons.pokit.domain.usecase.pokit.GetPokitImagesUseCase +import pokitmons.pokit.domain.usecase.pokit.GetPokitUseCase +import pokitmons.pokit.domain.usecase.pokit.GetPokitsUseCase +import pokitmons.pokit.domain.usecase.pokit.ModifyPokitUseCase +import javax.inject.Inject -class AddPokitViewModel : ContainerHost, ViewModel() { +@HiltViewModel +class AddPokitViewModel @Inject constructor( + private val getPokitImagesUseCase: GetPokitImagesUseCase, + private val getPokitsUseCase: GetPokitsUseCase, + private val getPokitUseCase: GetPokitUseCase, + private val createPokitUseCase: CreatePokitUseCase, + private val modifyPokitUseCase: ModifyPokitUseCase, + private val errorMessageProvider: ErrorMessageProvider, + savedStateHandle: SavedStateHandle, +) : ContainerHost, ViewModel() { override val container: Container = container(AddPokitScreenState()) + private val pokitId = savedStateHandle.get("pokit_id")?.toIntOrNull() + + private val pokitPaging = PokitPaging( + getPokits = getPokitsUseCase, + perPage = 10, + coroutineScope = viewModelScope, + initPage = 0 + ) + private val _pokitName = MutableStateFlow("") val pokitName: StateFlow = _pokitName.asStateFlow() + val pokitList: StateFlow> = pokitPaging.pagingData + val pokitListState: StateFlow = pokitPaging.pagingState + + private val _pokitIamges = MutableStateFlow>(emptyList()) + val pokitImages: StateFlow> = _pokitIamges.asStateFlow() + init { loadPokitList() + loadPokitImages() + + setAddModifyMode(pokitId) } - private fun loadPokitList() = intent { + fun loadPokitList() { viewModelScope.launch { - reduce { - state.copy( - step = AddPokitScreenStep.POKIT_LIST_LOADING, - pokitInputErrorMessage = null, - pokitList = emptyList() - ) + pokitPaging.load() + } + } + + private fun loadPokitImages() { + viewModelScope.launch { + val response = getPokitImagesUseCase.getImages() + if (response is PokitResult.Success) { + _pokitIamges.update { response.result.map { PokitImage.fromDomainPokitImage(it) } } + + if (pokitId == null) { + val defaultPokitImage = PokitImage.fromDomainPokitImage(response.result[0]) + intent { + reduce { state.copy(pokitImage = defaultPokitImage) } + } + } + } else { + // 만약 이미지 로딩이 실패한다면....? } - // todo 포킷 리스트 로드 api 연동 - delay(1000L) + } + } + private fun setAddModifyMode(pokitId: Int?) = intent { + if (pokitId == null) { reduce { - state.copy( - step = AddPokitScreenStep.IDLE, - pokitList = samplePokitList - ) + state.copy(isModify = false) + } + } else { + val response = getPokitUseCase.getPokit(pokitId) + if (response is PokitResult.Success) { + reduce { + state.copy(isModify = true, pokitImage = PokitImage.fromDomainPokitImage(response.result.image)) + } + _pokitName.update { response.result.name } + } else { + postSideEffect(AddPokitSideEffect.OnNavigationBack) } } } @@ -58,16 +116,11 @@ class AddPokitViewModel : ContainerHost intent { val isInAvailableLength = pokitName.length > POKIT_NAME_MAX_LENGTH - val isDuplicatePokitName = state.pokitList.find { it.title == pokitName } != null - val errorMessage = if (isInAvailableLength) { - PokitInputErrorMessage.TEXT_LENGTH_LIMIT - } else if (isDuplicatePokitName) { - PokitInputErrorMessage.ALREADY_USED_POKIT_NAME - } else { - null + if (isInAvailableLength) { + val errorMessage = errorMessageProvider.getTextLengthErrorMessage() + reduce { state.copy(pokitInputErrorMessage = errorMessage) } } - reduce { state.copy(pokitInputErrorMessage = errorMessage) } } } @@ -75,12 +128,30 @@ class AddPokitViewModel : ContainerHost reduce { state.copy(step = AddPokitScreenStep.POKIT_SAVE_LOADING) } - // todo 포킷 저장 api 연동 - delay(1000L) - reduce { - state.copy(step = AddPokitScreenStep.IDLE) + + val currentPokitName = pokitName.value + val pokitImageId = state.pokitImage?.id ?: 0 + val response = if (pokitId != null) { + modifyPokitUseCase.modifyPokit(pokitId, currentPokitName, pokitImageId) + } else { + createPokitUseCase.createPokit(currentPokitName, pokitImageId) + } + + if (response is PokitResult.Success) { + reduce { state.copy(step = AddPokitScreenStep.IDLE) } + + val sideEffect = if (pokitId == null) { + AddPokitSideEffect.AddPokitSuccess + } else { + AddPokitSideEffect.ModifyPokitSuccess(pokitId) + } + + postSideEffect(sideEffect) + } else { + response as PokitResult.Error + val errorMessage = errorMessageProvider.errorCodeToMessage(response.error.code) + reduce { state.copy(pokitInputErrorMessage = errorMessage) } } - postSideEffect(AddPokitSideEffect.AddPokitSuccess) } fun onBackPressed() = intent { @@ -108,9 +179,9 @@ class AddPokitViewModel : ContainerHost } } - fun selectPoktiProfile(pokitProfile: PokitProfile) = intent { + fun selectPokitProfile(pokitImage: PokitImage) = intent { reduce { - state.copy(pokitProfile = pokitProfile) + state.copy(pokitImage = pokitImage) } } } diff --git a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/Preview.kt b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/Preview.kt index dbb00c58..1385f62c 100644 --- a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/Preview.kt +++ b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/Preview.kt @@ -6,7 +6,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.strayalpaca.addpokit.model.AddPokitScreenState -import com.strayalpaca.addpokit.model.samplePokitList +import com.strayalpaca.addpokit.model.Pokit import pokitmons.pokit.core.ui.theme.PokitTheme @Preview(showBackground = true) @@ -17,8 +17,17 @@ fun Preview() { modifier = Modifier.fillMaxSize() ) { AddPokitScreen( - state = AddPokitScreenState().copy(pokitList = samplePokitList) + state = AddPokitScreenState(), + pokits = samplePokitList ) } } } + +private val samplePokitList = listOf( + Pokit(title = "안드로이드", id = "1", count = 2), + Pokit(title = "IOS", id = "2", count = 2), + Pokit(title = "디자인", id = "3", count = 2), + Pokit(title = "PM", id = "4", count = 1), + Pokit(title = "서버", id = "5", count = 2) +) diff --git a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/components/atom/PokitProfileImage.kt b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/components/atom/PokitProfileImage.kt index 91579c71..b5e9915b 100644 --- a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/components/atom/PokitProfileImage.kt +++ b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/components/atom/PokitProfileImage.kt @@ -1,25 +1,24 @@ package com.strayalpaca.addpokit.components.atom -import androidx.compose.foundation.Image import androidx.compose.foundation.border import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp -import com.strayalpaca.addpokit.model.PokitProfile +import coil.compose.AsyncImage +import com.strayalpaca.addpokit.model.PokitImage import pokitmons.pokit.core.ui.theme.PokitTheme -import pokitmons.pokit.core.ui.R.drawable as coreDrawable @Composable fun PokitProfileImage( - pokitProfile: PokitProfile, - onClick: (PokitProfile) -> Unit, + pokitImage: PokitImage, + onClick: (PokitImage) -> Unit, focused: Boolean = false, ) { val activeStrokeColor = PokitTheme.colors.brand @@ -31,18 +30,19 @@ fun PokitProfileImage( } } - Image( - painter = painterResource(id = coreDrawable.icon_24_plus_r), + AsyncImage( + model = pokitImage.url, contentDescription = "pokit profile image", + contentScale = ContentScale.Crop, modifier = Modifier - .size(66.dp) + .aspectRatio(1f) .clip(shape = RoundedCornerShape(12.dp)) .clickable { - onClick(pokitProfile) + onClick(pokitImage) } .border( color = strokeColor, - width = 1.dp, + width = 2.dp, shape = RoundedCornerShape(12.dp) ) ) diff --git a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/model/AddPokitScreenState.kt b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/model/AddPokitScreenState.kt index 511ee69c..5bbcbf05 100644 --- a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/model/AddPokitScreenState.kt +++ b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/model/AddPokitScreenState.kt @@ -1,29 +1,23 @@ package com.strayalpaca.addpokit.model import androidx.compose.runtime.Immutable -import com.strayalpaca.addpokit.R @Immutable data class AddPokitScreenState( - val pokitInputErrorMessage: PokitInputErrorMessage? = null, - val pokitList: List = emptyList(), - val step: AddPokitScreenStep = AddPokitScreenStep.POKIT_LIST_LOADING, - val pokitProfile: PokitProfile? = null, + val pokitInputErrorMessage: String? = null, + val step: AddPokitScreenStep = AddPokitScreenStep.IDLE, + val pokitImage: PokitImage? = null, + val isModify: Boolean = false, ) sealed class AddPokitScreenStep { data object IDLE : AddPokitScreenStep() - data object POKIT_LIST_LOADING : AddPokitScreenStep() data object POKIT_SAVE_LOADING : AddPokitScreenStep() data object SELECT_PROFILE : AddPokitScreenStep() } sealed class AddPokitSideEffect { data object AddPokitSuccess : AddPokitSideEffect() + data class ModifyPokitSuccess(val id: Int) : AddPokitSideEffect() data object OnNavigationBack : AddPokitSideEffect() } - -enum class PokitInputErrorMessage(val resourceId: Int) { - TEXT_LENGTH_LIMIT(R.string.text_length_limit_format), - ALREADY_USED_POKIT_NAME(R.string.already_used_pokit_name), -} diff --git a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/model/Pokit.kt b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/model/Pokit.kt index 1b004274..beda1492 100644 --- a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/model/Pokit.kt +++ b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/model/Pokit.kt @@ -4,12 +4,14 @@ data class Pokit( val title: String, val id: String, val count: Int, -) - -internal val samplePokitList = listOf( - Pokit(title = "안드로이드", id = "1", count = 2), - Pokit(title = "IOS", id = "2", count = 2), - Pokit(title = "디자인", id = "3", count = 2), - Pokit(title = "PM", id = "4", count = 1), - Pokit(title = "서버", id = "5", count = 2) -) +) { + companion object { + fun fromDomainPokit(pokit: pokitmons.pokit.domain.model.pokit.Pokit): Pokit { + return Pokit( + title = pokit.name, + id = pokit.categoryId.toString(), + count = pokit.linkCount + ) + } + } +} diff --git a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/model/PokitImage.kt b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/model/PokitImage.kt new file mode 100644 index 00000000..e1dce719 --- /dev/null +++ b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/model/PokitImage.kt @@ -0,0 +1,14 @@ +package com.strayalpaca.addpokit.model + +import pokitmons.pokit.domain.model.pokit.Pokit.Image as DomainPokitImage + +data class PokitImage( + val id: Int, + val url: String, +) { + companion object { + fun fromDomainPokitImage(domainPokitImage: DomainPokitImage): PokitImage { + return PokitImage(id = domainPokitImage.id, url = domainPokitImage.url) + } + } +} diff --git a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/model/PokitProfile.kt b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/model/PokitProfile.kt deleted file mode 100644 index 41274e56..00000000 --- a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/model/PokitProfile.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.strayalpaca.addpokit.model - -data class PokitProfile( - val id: String, -) - -internal val samplePokitProfileList = - listOf( - PokitProfile("1"), PokitProfile("2"), PokitProfile("3"), PokitProfile("4"), - PokitProfile("5"), PokitProfile("6"), PokitProfile("7"), PokitProfile("8"), - PokitProfile("9"), PokitProfile("10"), PokitProfile("11"), PokitProfile("12"), - PokitProfile("13"), PokitProfile("14"), PokitProfile("15") - ) diff --git a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/paging/PokitPaging.kt b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/paging/PokitPaging.kt new file mode 100644 index 00000000..e2661e50 --- /dev/null +++ b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/paging/PokitPaging.kt @@ -0,0 +1,124 @@ +package com.strayalpaca.addpokit.paging + +import com.strayalpaca.addpokit.model.Pokit +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.usecase.pokit.GetPokitsUseCase + +class PokitPaging( + private val getPokits: GetPokitsUseCase, + private val perPage: Int = 10, + private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO), + private val initPage: Int = 0, + private val firstRequestPage: Int = 3, +) : SimplePaging { + private val _pagingState = MutableStateFlow(SimplePagingState.IDLE) + override val pagingState: StateFlow = _pagingState.asStateFlow() + + private val _pagingData: MutableStateFlow> = MutableStateFlow(emptyList()) + override val pagingData: StateFlow> = _pagingData.asStateFlow() + private var currentPageIndex = initPage + private var requestJob: Job? = null + + override suspend fun refresh() { + requestJob?.cancel() + + _pagingData.update { emptyList() } + _pagingState.update { SimplePagingState.LOADING_INIT } + requestJob = coroutineScope.launch { + try { + currentPageIndex = initPage + val response = getPokits.getPokits(size = perPage * firstRequestPage, page = currentPageIndex) + when (response) { + is PokitResult.Success -> { + val pokitList = response.result.map { domainPokit -> + Pokit.fromDomainPokit(domainPokit) + } + applyResponse(pokitList, firstRequestPage) + } + + is PokitResult.Error -> { + _pagingState.update { SimplePagingState.FAILURE_INIT } + } + } + } catch (exception: Exception) { + if (exception !is CancellationException) { + _pagingState.update { SimplePagingState.FAILURE_INIT } + } + } + } + } + + override suspend fun load() { + if (pagingState.value != SimplePagingState.IDLE) return + + requestJob?.cancel() + _pagingState.update { SimplePagingState.LOADING_NEXT } + + requestJob = coroutineScope.launch { + try { + val response = getPokits.getPokits(size = perPage, page = currentPageIndex) + when (response) { + is PokitResult.Success -> { + val pokitList = response.result.map { domainPokit -> + Pokit.fromDomainPokit(domainPokit) + } + applyResponse(pokitList) + } + + is PokitResult.Error -> { + _pagingState.update { SimplePagingState.FAILURE_NEXT } + } + } + } catch (exception: Exception) { + if (exception !is CancellationException) { + _pagingState.update { SimplePagingState.FAILURE_NEXT } + } + } + } + } + + private fun applyResponse(dataInResponse: List, multiple: Int = 1) { + if (dataInResponse.size < perPage * multiple) { + _pagingState.update { SimplePagingState.LAST } + } else { + _pagingState.update { SimplePagingState.IDLE } + } + _pagingData.update { _pagingData.value + dataInResponse } + currentPageIndex += multiple + } + + override fun clear() { + requestJob?.cancel() + _pagingData.update { emptyList() } + _pagingState.update { SimplePagingState.IDLE } + } + + override suspend fun deleteItem(item: Pokit) { + val capturedDataList = _pagingData.value + _pagingData.update { capturedDataList.filter { it.id != item.id } } + } + + override suspend fun modifyItem(item: Pokit) { + val capturedDataList = _pagingData.value + val targetPokit = capturedDataList.find { it.id == item.id } ?: return + + _pagingData.update { + capturedDataList.map { pokit -> + if (targetPokit.id == pokit.id) { + item + } else { + pokit + } + } + } + } +} diff --git a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/paging/SimplePaging.kt b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/paging/SimplePaging.kt new file mode 100644 index 00000000..510374f3 --- /dev/null +++ b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/paging/SimplePaging.kt @@ -0,0 +1,13 @@ +package com.strayalpaca.addpokit.paging + +import kotlinx.coroutines.flow.Flow + +interface SimplePaging { + val pagingData: Flow> + suspend fun refresh() + suspend fun load() + val pagingState: Flow + suspend fun modifyItem(item: T) + suspend fun deleteItem(item: T) + fun clear() +} diff --git a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/paging/SimplePagingState.kt b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/paging/SimplePagingState.kt new file mode 100644 index 00000000..6ac52825 --- /dev/null +++ b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/paging/SimplePagingState.kt @@ -0,0 +1,5 @@ +package com.strayalpaca.addpokit.paging + +enum class SimplePagingState { + IDLE, LOADING_NEXT, LOADING_INIT, FAILURE_NEXT, FAILURE_INIT, LAST +} diff --git a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/utils/ErrorMessageProvider.kt b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/utils/ErrorMessageProvider.kt new file mode 100644 index 00000000..6f2dea63 --- /dev/null +++ b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/utils/ErrorMessageProvider.kt @@ -0,0 +1,38 @@ +package com.strayalpaca.addpokit.utils + +import android.content.Context +import com.strayalpaca.addpokit.R +import dagger.hilt.android.qualifiers.ApplicationContext +import pokitmons.pokit.domain.model.pokit.PokitErrorCode +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ErrorMessageProvider @Inject constructor(@ApplicationContext private val context: Context) { + fun errorCodeToMessage(errorCode: String): String { + return when (errorCode) { + PokitErrorCode.ALREADY_USED_POKIT_NAME -> { + context.getString(R.string.already_used_pokit_name) + } + PokitErrorCode.CANNOT_FOUND_POKIT_INFO -> { + context.getString(R.string.cannot_found_pokit_info) + } + PokitErrorCode.UNAVAILABLE_POKIT -> { + context.getString(R.string.unavailable_pokit) + } + PokitErrorCode.CANNOT_FOUND_POKIT_IMAGE -> { + context.getString(R.string.cannot_found_pokit_image) + } + PokitErrorCode.TOO_MUCH_POKIT -> { + context.getString(R.string.too_much_pokit) + } + else -> { + context.getString(R.string.uncategorized_error) + } + } + } + + fun getTextLengthErrorMessage(): String { + return context.getString(R.string.text_length_limit_format) + } +} diff --git a/feature/addpokit/src/main/res/values/string.xml b/feature/addpokit/src/main/res/values/string.xml index 09e348fc..28803499 100644 --- a/feature/addpokit/src/main/res/values/string.xml +++ b/feature/addpokit/src/main/res/values/string.xml @@ -1,6 +1,7 @@ 포킷 추가 + 포킷 수정 포킷 이름 카테고리 이름을 입력해주세요. 저장하기 @@ -8,4 +9,9 @@ 최대 10자까지 입력 가능합니다. 사용 중인 포킷명입니다. + 포킷 정보를 찾을 수 없습니다. + 사용할 수 없는 포킷명입니다. + 최대 30개의 포킷을 생성할 수 있습니다. 포킷을 삭제한 뒤에 추가해주세요. + 포킷 이미지 정보를 찾을 수 없습니다. + 네트워크 통신이 불안정합니다. 잠시 후 다시 시도해주세요. \ No newline at end of file diff --git a/feature/alarm/.gitignore b/feature/alarm/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/alarm/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/alarm/build.gradle.kts b/feature/alarm/build.gradle.kts new file mode 100644 index 00000000..71d3f2d6 --- /dev/null +++ b/feature/alarm/build.gradle.kts @@ -0,0 +1,74 @@ +plugins { + alias(libs.plugins.com.android.library) + alias(libs.plugins.org.jetbrains.kotlin.android) + alias(libs.plugins.hilt) + id("kotlin-kapt") +} + +android { + namespace = "pokitmons.pokit.alarm" + compileSdk = 34 + + defaultConfig { + minSdk = 24 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.1" + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) + + implementation(libs.orbit.compose) + implementation(libs.orbit.core) + implementation(libs.orbit.viewmodel) + + // hilt + implementation(libs.hilt) + kapt(libs.hilt.compiler) + + // coil + implementation(libs.coil.compose) + + implementation(project(":core:ui")) + implementation(project(":domain")) +} diff --git a/feature/alarm/consumer-rules.pro b/feature/alarm/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/feature/alarm/proguard-rules.pro b/feature/alarm/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/alarm/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/alarm/src/androidTest/java/pokitmons/pokit/alarm/ExampleInstrumentedTest.kt b/feature/alarm/src/androidTest/java/pokitmons/pokit/alarm/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..5e6a17a9 --- /dev/null +++ b/feature/alarm/src/androidTest/java/pokitmons/pokit/alarm/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package pokitmons.pokit.alarm + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("pokitmons.pokit.alarm.test", appContext.packageName) + } +} diff --git a/feature/alarm/src/main/AndroidManifest.xml b/feature/alarm/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/alarm/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/alarm/src/main/java/pokitmons/pokit/alarm/AlarmScreen.kt b/feature/alarm/src/main/java/pokitmons/pokit/alarm/AlarmScreen.kt new file mode 100644 index 00000000..14ff731d --- /dev/null +++ b/feature/alarm/src/main/java/pokitmons/pokit/alarm/AlarmScreen.kt @@ -0,0 +1,99 @@ +package pokitmons.pokit.alarm + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import pokitmons.pokit.alarm.components.alarmitem.AlarmItem +import pokitmons.pokit.alarm.components.toolbar.Toolbar +import pokitmons.pokit.alarm.model.Alarm +import pokitmons.pokit.alarm.paging.SimplePagingState + +@Composable +fun AlarmScreenContainer( + viewModel: AlarmViewModel, + onNavigateToLinkModify: (String) -> Unit = {}, + onBackPressed: () -> Unit, +) { + val alarms by viewModel.alarms.collectAsState() + val alarmsState by viewModel.alarmsState.collectAsState() + + AlarmScreen( + onClickBack = onBackPressed, + onClickAlarm = remember { + { alarmId -> + viewModel.readAlarm(alarmId) + onNavigateToLinkModify(alarmId) + } + }, + onClickAlarmRemove = viewModel::removeAlarm, + alarms = alarms, + alarmsState = alarmsState, + loadNextAlarms = viewModel::loadNextAlarms + ) +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun AlarmScreen( + onClickBack: () -> Unit = {}, + onClickAlarm: (String) -> Unit = {}, + onClickAlarmRemove: (String) -> Unit = {}, + alarms: List = emptyList(), + alarmsState: SimplePagingState = SimplePagingState.IDLE, + loadNextAlarms: () -> Unit = {}, +) { + Column( + modifier = Modifier.fillMaxSize() + ) { + Toolbar( + onClickBack = onClickBack, + title = stringResource(id = R.string.alarm_box) + ) + + val alarmLazyColumnListState = rememberLazyListState() + val startAlarmPaging = remember { + derivedStateOf { + alarmLazyColumnListState.layoutInfo.visibleItemsInfo.lastOrNull()?.let { last -> + last.index >= alarmLazyColumnListState.layoutInfo.totalItemsCount - 3 + } ?: false + } + } + + LaunchedEffect(startAlarmPaging.value) { + if (startAlarmPaging.value && alarmsState == SimplePagingState.IDLE) { + loadNextAlarms() + } + } + + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + state = alarmLazyColumnListState + ) { + items( + items = alarms, + key = { alarm -> alarm.id } + ) { alarm -> + AlarmItem( + modifier = Modifier.animateItemPlacement(), + alarm = alarm, + onClickAlarm = onClickAlarm, + onClickRemove = onClickAlarmRemove + ) + } + } + } +} diff --git a/feature/alarm/src/main/java/pokitmons/pokit/alarm/AlarmViewModel.kt b/feature/alarm/src/main/java/pokitmons/pokit/alarm/AlarmViewModel.kt new file mode 100644 index 00000000..da09f4d2 --- /dev/null +++ b/feature/alarm/src/main/java/pokitmons/pokit/alarm/AlarmViewModel.kt @@ -0,0 +1,60 @@ +package pokitmons.pokit.alarm + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import pokitmons.pokit.alarm.model.Alarm +import pokitmons.pokit.alarm.paging.AlarmPaging +import pokitmons.pokit.alarm.paging.SimplePagingState +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.usecase.alert.DeleteAlertUseCase +import pokitmons.pokit.domain.usecase.alert.GetAlertsUseCase +import javax.inject.Inject + +@HiltViewModel +class AlarmViewModel @Inject constructor( + getAlertsUseCase: GetAlertsUseCase, + private val deleteAlertUseCase: DeleteAlertUseCase, +) : ViewModel() { + + private val alarmPaging = AlarmPaging(getAlertsUseCase = getAlertsUseCase) + + val alarms: StateFlow> = alarmPaging.pagingData + val alarmsState: StateFlow = alarmPaging.pagingState + + init { + viewModelScope.launch { + alarmPaging.refresh() + } + } + + fun removeAlarm(alarmId: String) { + val id = alarmId.toIntOrNull() ?: return + viewModelScope.launch { + val response = deleteAlertUseCase.deleteAlert(id) + if (response is PokitResult.Success) { + viewModelScope.launch { + alarms.value.find { it.id == alarmId }?.let { targetItem -> + alarmPaging.deleteItem(targetItem) + } + } + } + } + } + + fun loadNextAlarms() { + viewModelScope.launch { + alarmPaging.load() + } + } + + fun readAlarm(alarmId: String) { + val targetAlarm = alarms.value.find { it.id == alarmId } ?: return + + viewModelScope.launch { + alarmPaging.modifyItem(item = targetAlarm.copy(read = true)) + } + } +} diff --git a/feature/alarm/src/main/java/pokitmons/pokit/alarm/Preview.kt b/feature/alarm/src/main/java/pokitmons/pokit/alarm/Preview.kt new file mode 100644 index 00000000..be65ba09 --- /dev/null +++ b/feature/alarm/src/main/java/pokitmons/pokit/alarm/Preview.kt @@ -0,0 +1,19 @@ +package pokitmons.pokit.alarm + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import pokitmons.pokit.alarm.model.Alarm +import pokitmons.pokit.core.ui.theme.PokitTheme + +@Preview(showBackground = true) +@Composable +internal fun Preview() { + PokitTheme { + Column { + AlarmScreen( + alarms = listOf(Alarm(id = "1", title = "title1", thumbnail = ""), Alarm(id = "2", title = "title2", thumbnail = "")) + ) + } + } +} diff --git a/feature/alarm/src/main/java/pokitmons/pokit/alarm/components/alarmitem/AlarmItem.kt b/feature/alarm/src/main/java/pokitmons/pokit/alarm/components/alarmitem/AlarmItem.kt new file mode 100644 index 00000000..590133f1 --- /dev/null +++ b/feature/alarm/src/main/java/pokitmons/pokit/alarm/components/alarmitem/AlarmItem.kt @@ -0,0 +1,142 @@ +package pokitmons.pokit.alarm.components.alarmitem + +import androidx.compose.animation.core.tween +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.AnchoredDraggableState +import androidx.compose.foundation.gestures.DraggableAnchors +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.anchoredDraggable +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.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import coil.compose.rememberAsyncImagePainter +import pokitmons.pokit.alarm.R +import pokitmons.pokit.alarm.model.Alarm +import pokitmons.pokit.alarm.util.diffString +import pokitmons.pokit.core.ui.components.block.pushcard.PushCard +import pokitmons.pokit.core.ui.theme.PokitTheme +import kotlin.math.roundToInt +import pokitmons.pokit.core.ui.R.drawable as coreDrawable + +enum class DragValue { Expanded, Center } + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun AlarmItem( + modifier: Modifier = Modifier, + alarm: Alarm, + onClickAlarm: (String) -> Unit, + onClickRemove: (String) -> Unit, +) { + val density = LocalDensity.current + val dragMaxOffset = with(density) { + (-60).dp.toPx() + } + val draggableState = remember { + AnchoredDraggableState( + anchors = DraggableAnchors { + DragValue.Expanded at dragMaxOffset + DragValue.Center at 0f + }, + initialValue = DragValue.Center, + animationSpec = tween(), + positionalThreshold = { totalDistance -> totalDistance * 0.5f }, + velocityThreshold = { with(density) { 60.dp.toPx() } } + ) + } + val timeDiffString = diffString(createdAtCalendar = alarm.createdAt.getCalendar()) + + Column( + modifier = modifier.fillMaxWidth() + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min) + ) { + Column( + modifier = Modifier + .fillMaxHeight() + .width(60.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { onClickRemove(alarm.id) } + ) + .background(PokitTheme.colors.error) + .align(Alignment.CenterEnd), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(id = coreDrawable.icon_24_trash), + contentDescription = "", + colorFilter = ColorFilter.tint(PokitTheme.colors.inverseWh), + modifier = Modifier.size(24.dp) + ) + Text( + text = stringResource(id = R.string.remove), + style = PokitTheme.typography.body3Medium.copy(color = PokitTheme.colors.inverseWh) + ) + } + + Row( + modifier = Modifier + .offset { + IntOffset( + x = draggableState + .requireOffset() + .roundToInt(), + y = 0 + ) + } + .anchoredDraggable( + state = draggableState, + orientation = Orientation.Horizontal + ) + .fillMaxWidth() + .background(PokitTheme.colors.backgroundBase), + verticalAlignment = Alignment.CenterVertically + ) { + PushCard( + title = alarm.title, + sub = "여기 문구 뭘로 하나요?\n뭐가 들어가야 됨????", + timeString = timeDiffString, + painter = rememberAsyncImagePainter(alarm.thumbnail), + read = !alarm.read, + onClick = remember { + { + onClickAlarm(alarm.id) + } + } + ) + } + } + + HorizontalDivider(thickness = 1.dp, color = PokitTheme.colors.borderTertiary) + } +} diff --git a/feature/alarm/src/main/java/pokitmons/pokit/alarm/components/alarmitem/Preview.kt b/feature/alarm/src/main/java/pokitmons/pokit/alarm/components/alarmitem/Preview.kt new file mode 100644 index 00000000..9dc24f52 --- /dev/null +++ b/feature/alarm/src/main/java/pokitmons/pokit/alarm/components/alarmitem/Preview.kt @@ -0,0 +1,20 @@ +package pokitmons.pokit.alarm.components.alarmitem + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import pokitmons.pokit.alarm.model.Alarm +import pokitmons.pokit.alarm.model.Date +import pokitmons.pokit.core.ui.theme.PokitTheme + +@Preview(showBackground = true) +@Composable +internal fun Preview() { + PokitTheme { + Column { + AlarmItem(alarm = Alarm(title = "자연 친화적인 라이프스타일을 위한 무언가"), onClickAlarm = {}, onClickRemove = {}) + AlarmItem(alarm = Alarm(title = "자연 친화적인 라이프스타일을 위한 무언가", read = true, createdAt = Date(year = 1969)), onClickAlarm = {}, onClickRemove = {}) + AlarmItem(alarm = Alarm(title = "자연 친화적인 라이프스타일을 위한 무언가", createdAt = Date(year = 1960)), onClickAlarm = {}, onClickRemove = {}) + } + } +} diff --git a/feature/alarm/src/main/java/pokitmons/pokit/alarm/components/toolbar/Preview.kt b/feature/alarm/src/main/java/pokitmons/pokit/alarm/components/toolbar/Preview.kt new file mode 100644 index 00000000..a9190a55 --- /dev/null +++ b/feature/alarm/src/main/java/pokitmons/pokit/alarm/components/toolbar/Preview.kt @@ -0,0 +1,18 @@ +package pokitmons.pokit.alarm.components.toolbar + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import pokitmons.pokit.alarm.R +import pokitmons.pokit.core.ui.theme.PokitTheme + +@Preview(showBackground = true) +@Composable +internal fun Preview() { + PokitTheme { + Column { + Toolbar(onClickBack = {}, title = stringResource(id = R.string.alarm_box)) + } + } +} diff --git a/feature/alarm/src/main/java/pokitmons/pokit/alarm/components/toolbar/Toolbar.kt b/feature/alarm/src/main/java/pokitmons/pokit/alarm/components/toolbar/Toolbar.kt new file mode 100644 index 00000000..9d0f6cc7 --- /dev/null +++ b/feature/alarm/src/main/java/pokitmons/pokit/alarm/components/toolbar/Toolbar.kt @@ -0,0 +1,48 @@ +package pokitmons.pokit.alarm.components.toolbar + +import androidx.compose.foundation.layout.Box +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.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.theme.PokitTheme +import pokitmons.pokit.core.ui.R.drawable as coreDrawable + +@Composable +internal fun Toolbar( + onClickBack: () -> Unit, + title: String, +) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + .padding(horizontal = 12.dp) + ) { + IconButton( + modifier = Modifier + .size(48.dp) + .align(Alignment.CenterStart), + onClick = onClickBack + ) { + Icon( + painter = painterResource(id = coreDrawable.icon_24_arrow_left), + contentDescription = "back button" + ) + } + + Text( + modifier = Modifier.align(Alignment.Center), + text = title, + style = PokitTheme.typography.title3.copy(color = PokitTheme.colors.textPrimary) + ) + } +} diff --git a/feature/alarm/src/main/java/pokitmons/pokit/alarm/model/Alarm.kt b/feature/alarm/src/main/java/pokitmons/pokit/alarm/model/Alarm.kt new file mode 100644 index 00000000..502379de --- /dev/null +++ b/feature/alarm/src/main/java/pokitmons/pokit/alarm/model/Alarm.kt @@ -0,0 +1,24 @@ +package pokitmons.pokit.alarm.model + +import pokitmons.pokit.domain.model.alert.Alarm as DomainAlarm + +data class Alarm( + val id: String = "", + val contentId: String = "", + val title: String = "", + val thumbnail: String? = null, + val createdAt: Date = Date(), + val read: Boolean = false, +) { + companion object { + fun fromDomainAlarm(domainAlarm: DomainAlarm): Alarm { + return Alarm( + id = domainAlarm.id.toString(), + contentId = domainAlarm.contentId.toString(), + title = domainAlarm.title, + thumbnail = domainAlarm.thumbnail, + createdAt = Date.getInstanceFromString(domainAlarm.createdAt) + ) + } + } +} diff --git a/feature/alarm/src/main/java/pokitmons/pokit/alarm/model/Date.kt b/feature/alarm/src/main/java/pokitmons/pokit/alarm/model/Date.kt new file mode 100644 index 00000000..84c540ea --- /dev/null +++ b/feature/alarm/src/main/java/pokitmons/pokit/alarm/model/Date.kt @@ -0,0 +1,59 @@ +package pokitmons.pokit.alarm.model + +import android.icu.util.Calendar +import android.os.Build +import java.text.SimpleDateFormat +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.Locale + +data class Date( + val year: Int = 2024, + val month: Int = 8, + val day: Int = 17, + val hour: Int = 15, + val minute: Int = 41, +) { + fun getCalendar(): Calendar { + return Calendar.getInstance().apply { + set(Calendar.YEAR, year) + set(Calendar.MONTH, month - 1) + set(Calendar.DAY_OF_MONTH, day) + set(Calendar.HOUR_OF_DAY, hour) + set(Calendar.MINUTE, minute) + } + } + + companion object { + fun getInstanceFromString( + createdAtString: String, + pattern: String = "yyyy-MM-dd HH:mm:ss", + ): Date { + try { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val formatter = DateTimeFormatter.ofPattern(pattern) + val dateTime = LocalDateTime.parse(createdAtString, formatter) + Date( + year = dateTime.year, + month = dateTime.monthValue, + day = dateTime.dayOfMonth, + hour = dateTime.hour, + minute = dateTime.minute + ) + } else { + val date = SimpleDateFormat(pattern, Locale.KOREA).parse(createdAtString) + val calendar = Calendar.getInstance().apply { time = date } + Date( + year = calendar.get(Calendar.YEAR), + month = calendar.get(Calendar.MONTH) + 1, + day = calendar.get(Calendar.DAY_OF_MONTH), + hour = calendar.get(Calendar.HOUR_OF_DAY), + minute = calendar.get(Calendar.MINUTE) + ) + } + } catch (e: Exception) { + return Date(year = 2000, month = 1, day = 1, hour = 0, minute = 0) + } + } + } +} diff --git a/feature/alarm/src/main/java/pokitmons/pokit/alarm/paging/AlarmPaging.kt b/feature/alarm/src/main/java/pokitmons/pokit/alarm/paging/AlarmPaging.kt new file mode 100644 index 00000000..b2d21c1a --- /dev/null +++ b/feature/alarm/src/main/java/pokitmons/pokit/alarm/paging/AlarmPaging.kt @@ -0,0 +1,125 @@ +package pokitmons.pokit.alarm.paging + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import pokitmons.pokit.alarm.model.Alarm +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.usecase.alert.GetAlertsUseCase +import kotlin.coroutines.cancellation.CancellationException + +class AlarmPaging( + private val getAlertsUseCase: GetAlertsUseCase, + private val perPage: Int = 10, + private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO), + private val initPage: Int = 0, + private val firstRequestPage: Int = 3, +) : SimplePaging { + private val _pagingData: MutableStateFlow> = MutableStateFlow(emptyList()) + override val pagingData: StateFlow> = _pagingData.asStateFlow() + + private val _pagingState: MutableStateFlow = MutableStateFlow(SimplePagingState.IDLE) + override val pagingState: StateFlow = _pagingState.asStateFlow() + + private var currentPageIndex = initPage + private var requestJob: Job? = null + + override suspend fun refresh() { + requestJob?.cancel() + + _pagingData.update { emptyList() } + _pagingState.update { SimplePagingState.LOADING_INIT } + requestJob = coroutineScope.launch { + try { + currentPageIndex = initPage + val response = getAlertsUseCase.getAlerts(size = perPage * firstRequestPage, page = currentPageIndex) + when (response) { + is PokitResult.Success -> { + val alarms = response.result.map { domainAlarm -> + Alarm.fromDomainAlarm(domainAlarm) + } + applyResponse(alarms, firstRequestPage) + } + + is PokitResult.Error -> { + _pagingState.update { SimplePagingState.FAILURE_INIT } + } + } + } catch (exception: Exception) { + if (exception !is CancellationException) { + _pagingState.update { SimplePagingState.FAILURE_INIT } + } + } + } + } + + override suspend fun load() { + if (pagingState.value != SimplePagingState.IDLE) return + + requestJob?.cancel() + _pagingState.update { SimplePagingState.LOADING_NEXT } + + requestJob = coroutineScope.launch { + try { + val response = getAlertsUseCase.getAlerts(size = perPage, page = currentPageIndex) + when (response) { + is PokitResult.Success -> { + val alarms = response.result.map { domainAlarm -> + Alarm.fromDomainAlarm(domainAlarm) + } + applyResponse(alarms) + } + + is PokitResult.Error -> { + _pagingState.update { SimplePagingState.FAILURE_NEXT } + } + } + } catch (exception: Exception) { + if (exception !is CancellationException) { + _pagingState.update { SimplePagingState.FAILURE_NEXT } + } + } + } + } + + private fun applyResponse(dataInResponse: List, multiple: Int = 1) { + if (dataInResponse.size < perPage * multiple) { + _pagingState.update { SimplePagingState.LAST } + } else { + _pagingState.update { SimplePagingState.IDLE } + } + _pagingData.update { _pagingData.value + dataInResponse } + currentPageIndex += multiple + } + + override fun clear() { + requestJob?.cancel() + _pagingData.update { emptyList() } + _pagingState.update { SimplePagingState.IDLE } + } + + override suspend fun deleteItem(item: Alarm) { + val capturedDataList = _pagingData.value + _pagingData.update { capturedDataList.filter { it.id != item.id } } + } + + override suspend fun modifyItem(item: Alarm) { + val capturedDataList = _pagingData.value + val targetData = capturedDataList.find { it.id == item.id } ?: return + + _pagingData.update { + capturedDataList.map { data -> + if (targetData.id == data.id) { + item + } else { + data + } + } + } + } +} diff --git a/feature/alarm/src/main/java/pokitmons/pokit/alarm/paging/SimplePaging.kt b/feature/alarm/src/main/java/pokitmons/pokit/alarm/paging/SimplePaging.kt new file mode 100644 index 00000000..8062d91c --- /dev/null +++ b/feature/alarm/src/main/java/pokitmons/pokit/alarm/paging/SimplePaging.kt @@ -0,0 +1,13 @@ +package pokitmons.pokit.alarm.paging + +import kotlinx.coroutines.flow.Flow + +interface SimplePaging { + val pagingData: Flow> + suspend fun refresh() + suspend fun load() + val pagingState: Flow + suspend fun modifyItem(item: T) + suspend fun deleteItem(item: T) + fun clear() +} diff --git a/feature/alarm/src/main/java/pokitmons/pokit/alarm/paging/SimplePagingState.kt b/feature/alarm/src/main/java/pokitmons/pokit/alarm/paging/SimplePagingState.kt new file mode 100644 index 00000000..29aeb08e --- /dev/null +++ b/feature/alarm/src/main/java/pokitmons/pokit/alarm/paging/SimplePagingState.kt @@ -0,0 +1,5 @@ +package pokitmons.pokit.alarm.paging + +enum class SimplePagingState { + IDLE, LOADING_NEXT, LOADING_INIT, FAILURE_NEXT, FAILURE_INIT, LAST +} diff --git a/feature/alarm/src/main/java/pokitmons/pokit/alarm/util/DateStringUtil.kt b/feature/alarm/src/main/java/pokitmons/pokit/alarm/util/DateStringUtil.kt new file mode 100644 index 00000000..aaabd291 --- /dev/null +++ b/feature/alarm/src/main/java/pokitmons/pokit/alarm/util/DateStringUtil.kt @@ -0,0 +1,36 @@ +package pokitmons.pokit.alarm.util + +import android.icu.util.Calendar +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import pokitmons.pokit.alarm.R + +private const val HOUR_SECOND = 60 * 60 +private const val DAY_SECOND = 24 * HOUR_SECOND +private const val MONTH_SECOND = 30 * DAY_SECOND +private const val YEAR_SECOND = 365 * DAY_SECOND + +@Composable +fun diffString(createdAtCalendar: Calendar, currentCalendar: Calendar = Calendar.getInstance()): String { + val diffTimeSecond = (currentCalendar.timeInMillis - createdAtCalendar.timeInMillis) / 1000 + return when { + (diffTimeSecond < 60) -> { + stringResource(id = R.string.just_now) + } + (diffTimeSecond < HOUR_SECOND) -> { + stringResource(id = R.string.minute_before, (diffTimeSecond / 60)) + } + (diffTimeSecond < DAY_SECOND) -> { + stringResource(id = R.string.hour_before, (diffTimeSecond / HOUR_SECOND)) + } + (diffTimeSecond < MONTH_SECOND) -> { + stringResource(id = R.string.day_before, (diffTimeSecond / DAY_SECOND)) + } + (diffTimeSecond < YEAR_SECOND) -> { + stringResource(id = R.string.month_before, (diffTimeSecond / MONTH_SECOND)) + } + else -> { + stringResource(id = R.string.year_before, (diffTimeSecond / YEAR_SECOND)) + } + } +} diff --git a/feature/alarm/src/main/res/values/string.xml b/feature/alarm/src/main/res/values/string.xml new file mode 100644 index 00000000..c985f0e2 --- /dev/null +++ b/feature/alarm/src/main/res/values/string.xml @@ -0,0 +1,12 @@ + + + 알림함 + 삭제 + + 지금 막 + %d분 전 + %시간 전 + %d일 전 + %d달 전 + %d년 전 + \ No newline at end of file diff --git a/feature/pokitdetail/src/test/java/com/strayalpaca/pokitdetail/ExampleUnitTest.kt b/feature/alarm/src/test/java/pokitmons/pokit/alarm/ExampleUnitTest.kt similarity index 90% rename from feature/pokitdetail/src/test/java/com/strayalpaca/pokitdetail/ExampleUnitTest.kt rename to feature/alarm/src/test/java/pokitmons/pokit/alarm/ExampleUnitTest.kt index 537a1244..3c7f69ab 100644 --- a/feature/pokitdetail/src/test/java/com/strayalpaca/pokitdetail/ExampleUnitTest.kt +++ b/feature/alarm/src/test/java/pokitmons/pokit/alarm/ExampleUnitTest.kt @@ -1,4 +1,4 @@ -package com.strayalpaca.pokitdetail +package pokitmons.pokit.alarm import org.junit.Assert.assertEquals import org.junit.Test diff --git a/feature/home/build.gradle.kts b/feature/home/build.gradle.kts new file mode 100644 index 00000000..4e02a173 --- /dev/null +++ b/feature/home/build.gradle.kts @@ -0,0 +1,77 @@ +plugins { + alias(libs.plugins.com.android.library) + alias(libs.plugins.org.jetbrains.kotlin.android) + alias(libs.plugins.hilt) + id("kotlin-kapt") +} + +android { + namespace = "pokitmons.pokit.home" + compileSdk = 34 + + defaultConfig { + minSdk = 24 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } + + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.5.1" + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + implementation(libs.androidx.navigation.compose) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) + + // hilt + implementation(libs.hilt) + kapt(libs.hilt.compiler) + + // navigation + implementation(libs.androidx.navigation.compose) + implementation(libs.hilt.navigation.compose) + + // coil + implementation(libs.coil.compose) + + // module + implementation(project(":core:ui")) + implementation(project(":domain")) + implementation(project(":feature:pokitdetail")) +} diff --git a/feature/home/proguard-rules.pro b/feature/home/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/home/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/home/src/androidTest/java/pokitmons/pokit/home/ExampleInstrumentedTest.kt b/feature/home/src/androidTest/java/pokitmons/pokit/home/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..ccdaae64 --- /dev/null +++ b/feature/home/src/androidTest/java/pokitmons/pokit/home/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package pokitmons.pokit.home + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import junit.framework.TestCase.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("pokitmons.pokit.home", appContext.packageName) + } +} diff --git a/feature/home/src/main/AndroidManifest.xml b/feature/home/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/home/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/home/src/main/java/pokitmons/pokit/home/BottomNavigationBar.kt b/feature/home/src/main/java/pokitmons/pokit/home/BottomNavigationBar.kt new file mode 100644 index 00000000..98a4a793 --- /dev/null +++ b/feature/home/src/main/java/pokitmons/pokit/home/BottomNavigationBar.kt @@ -0,0 +1,106 @@ +package pokitmons.pokit.home + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +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.shape.RoundedCornerShape +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import pokitmons.pokit.core.ui.theme.PokitTheme +import pokitmons.pokit.home.pokit.PokitViewModel +import pokitmons.pokit.home.pokit.ScreenType +import pokitmons.pokit.core.ui.R.drawable as DrawableResource + +// TODO : 바텀시트 아이템 컴포저블로 만들기 + +@Composable +fun BottomNavigationBar(viewModel: PokitViewModel = hiltViewModel()) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp), + shadowElevation = 20.dp + ) { + BottomAppBar( + containerColor = PokitTheme.colors.backgroundBase, + modifier = Modifier.height(92.dp), + tonalElevation = 8.dp + ) { + Column( + modifier = Modifier + .weight(2f) + .clickable { viewModel.updateScreenType(ScreenType.Pokit) } + .padding(bottom = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + painter = painterResource(id = DrawableResource.icon_24_folder), + contentDescription = "리마인드", + tint = when (viewModel.screenType.value) { + is ScreenType.Pokit -> Color.Black + is ScreenType.Remind -> PokitTheme.colors.iconTertiary + }, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + color = when (viewModel.screenType.value) { + is ScreenType.Pokit -> Color.Black + is ScreenType.Remind -> PokitTheme.colors.textTertiary + }, + style = PokitTheme.typography.detail2, + text = "포킷", + textAlign = TextAlign.Center + ) + } + + Column( + modifier = Modifier + .weight(2f) + .clickable { viewModel.updateScreenType(ScreenType.Remind) } + .padding(bottom = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + painter = painterResource(id = DrawableResource.icon_24_remind), + contentDescription = "리마인드", + tint = when (viewModel.screenType.value) { + is ScreenType.Remind -> Color.Black + is ScreenType.Pokit -> PokitTheme.colors.iconTertiary + }, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + color = when (viewModel.screenType.value) { + is ScreenType.Remind -> Color.Black + is ScreenType.Pokit -> PokitTheme.colors.textTertiary + }, + style = PokitTheme.typography.detail2, + text = "리마인드", + textAlign = TextAlign.Center + ) + } + } + } +} + +@Preview +@Composable +fun BottomNavigationBarPreview() { + BottomNavigationBar() +} diff --git a/feature/home/src/main/java/pokitmons/pokit/home/HomeHeader.kt b/feature/home/src/main/java/pokitmons/pokit/home/HomeHeader.kt new file mode 100644 index 00000000..92d663a5 --- /dev/null +++ b/feature/home/src/main/java/pokitmons/pokit/home/HomeHeader.kt @@ -0,0 +1,80 @@ +package pokitmons.pokit.home + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.R +import pokitmons.pokit.core.ui.theme.PokitTheme +import pokitmons.pokit.home.pokit.PokitViewModel +import pokitmons.pokit.home.pokit.ScreenType + +@Composable +fun HomeHeader( + viewModel: PokitViewModel, + onNavigateToSetting: () -> Unit, + onNavigateToSearch: () -> Unit, +) { + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier + .background(color = Color.White) + .fillMaxWidth() + .height(56.dp) + .padding(horizontal = 20.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = when (viewModel.screenType.value) { + is ScreenType.Pokit -> painterResource(id = R.drawable.logo_pokit) + is ScreenType.Remind -> painterResource(id = R.drawable.logo_remind) + }, + tint = PokitTheme.colors.brand, + contentDescription = "로고" + ) + + Spacer(modifier = Modifier.weight(1f)) + + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon( + painterResource(id = R.drawable.icon_24_search), + contentDescription = "검색", + modifier = Modifier + .size(24.dp) + .clickable { onNavigateToSearch() } + ) + Icon( + painterResource(id = R.drawable.icon_24_bell), + contentDescription = "알림", + modifier = Modifier.size(24.dp) + ) + + when (viewModel.screenType.value) { + is ScreenType.Pokit -> { + Icon( + painterResource(id = R.drawable.icon_24_setup), + contentDescription = "설정", + modifier = Modifier + .size(24.dp) + .clickable { onNavigateToSetting() } + ) + } + is ScreenType.Remind -> Unit + } + } + } +} diff --git a/feature/home/src/main/java/pokitmons/pokit/home/HomeScreen.kt b/feature/home/src/main/java/pokitmons/pokit/home/HomeScreen.kt new file mode 100644 index 00000000..2e247642 --- /dev/null +++ b/feature/home/src/main/java/pokitmons/pokit/home/HomeScreen.kt @@ -0,0 +1,183 @@ +package pokitmons.pokit.home + +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 +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +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 +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import pokitmons.pokit.core.ui.R +import pokitmons.pokit.core.ui.theme.PokitTheme +import pokitmons.pokit.home.pokit.PokitScreen +import pokitmons.pokit.home.pokit.PokitViewModel +import pokitmons.pokit.home.pokit.ScreenType +import pokitmons.pokit.home.remind.RemindScreen + +// TODO 화면 단으로 분라 +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HomeScreen( + viewModel: PokitViewModel, + onNavigateToPokitDetail: (String) -> Unit, + onNavigateToSearch: () -> Unit, + onNavigateToSetting: () -> Unit, + onNavigateAddLink: () -> Unit, + onNavigateAddPokit: () -> Unit, + +) { + val sheetState = rememberModalBottomSheetState() + val scope = rememberCoroutineScope() + var showBottomSheet by remember { mutableStateOf(false) } + + Box( + modifier = Modifier + .background(color = Color.White) + .fillMaxSize() + ) { + if (showBottomSheet) { + ModalBottomSheet( + onDismissRequest = { + showBottomSheet = false + }, + sheetState = sheetState + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(180.dp), + horizontalArrangement = Arrangement.Center + ) { + Column( + modifier = Modifier + .background( + color = PokitTheme.colors.brand, + shape = RoundedCornerShape(12.dp) + ) + .size(96.dp) + .clickable { + scope.launch { + sheetState.hide() + showBottomSheet = false + onNavigateAddLink() + } + }, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + tint = PokitTheme.colors.inverseWh, + modifier = Modifier.size(32.dp), + painter = painterResource(id = R.drawable.icon_24_link), + contentDescription = null + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + style = PokitTheme.typography.body3Medium, + text = "링크추가", + color = PokitTheme.colors.inverseWh + ) + } + + Spacer(modifier = Modifier.padding(horizontal = 10.dp)) + + Column( + modifier = Modifier + .background( + color = PokitTheme.colors.brand, + shape = RoundedCornerShape(12.dp) + ) + .size(96.dp) + .clickable { + scope.launch { + sheetState.hide() + showBottomSheet = false + onNavigateAddPokit() + } + }, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + tint = PokitTheme.colors.inverseWh, + modifier = Modifier.size(32.dp), + painter = painterResource(id = R.drawable.icon_24_folder), + contentDescription = null + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + style = PokitTheme.typography.body3Medium, + text = "포킷추가", + color = PokitTheme.colors.inverseWh + ) + } + } + } + } + + // screen + Column( + modifier = Modifier + .background(color = Color.White) + .fillMaxSize() + ) { + HomeHeader( + viewModel = viewModel, + onNavigateToSearch = { onNavigateToSearch() }, + onNavigateToSetting = { onNavigateToSetting() } + ) + Scaffold( + bottomBar = { BottomNavigationBar() } + ) { padding -> + when (viewModel.screenType.value) { + is ScreenType.Pokit -> { + PokitScreen( + viewModel = viewModel, + modifier = Modifier.padding(padding), + onNavigateToPokitDetail = onNavigateToPokitDetail + ) + } + + is ScreenType.Remind -> { + RemindScreen(Modifier.padding(padding)) + } + } + } + } + Image( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 60.dp) + .clickable { + showBottomSheet = true + }, + painter = painterResource(id = R.drawable.image_floating), + contentDescription = null + ) + } +} diff --git a/feature/home/src/main/java/pokitmons/pokit/home/pokit/CategorySelector.kt b/feature/home/src/main/java/pokitmons/pokit/home/pokit/CategorySelector.kt new file mode 100644 index 00000000..4f8ffbad --- /dev/null +++ b/feature/home/src/main/java/pokitmons/pokit/home/pokit/CategorySelector.kt @@ -0,0 +1,119 @@ +package pokitmons.pokit.home.pokit + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import pokitmons.pokit.core.ui.components.atom.button.PokitButton +import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonIcon +import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonIconPosition +import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonShape +import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonSize +import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonStyle +import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonType +import pokitmons.pokit.core.ui.theme.PokitTheme +import pokitmons.pokit.core.ui.R.drawable as DrawableResource + +@Composable +fun HomeMid(viewModel: PokitViewModel = hiltViewModel()) { + Spacer(modifier = Modifier.height(24.dp)) + Column( + modifier = Modifier + .fillMaxWidth() + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + PokitButton( + size = PokitButtonSize.SMALL, + style = when (viewModel.selectedCategory.value) { + is Category.Pokit -> PokitButtonStyle.FILLED + is Category.Unclassified -> PokitButtonStyle.DEFAULT + }, + text = "포킷", + shape = PokitButtonShape.ROUND, + icon = PokitButtonIcon( + DrawableResource.icon_24_folderline, + PokitButtonIconPosition.LEFT + ), + type = when (viewModel.selectedCategory.value) { + is Category.Pokit -> PokitButtonType.PRIMARY + is Category.Unclassified -> PokitButtonType.SECONDARY + }, + onClick = { viewModel.updateCategory(Category.Pokit) } + ) + + Spacer(modifier = Modifier.padding(start = 8.dp)) + + PokitButton( + size = PokitButtonSize.SMALL, + style = when (viewModel.selectedCategory.value) { + is Category.Pokit -> PokitButtonStyle.DEFAULT + is Category.Unclassified -> PokitButtonStyle.FILLED + }, + text = "미분류", + shape = PokitButtonShape.ROUND, + icon = PokitButtonIcon( + DrawableResource.icon_24_info, + PokitButtonIconPosition.LEFT + ), + type = when (viewModel.selectedCategory.value) { + is Category.Pokit -> PokitButtonType.SECONDARY + is Category.Unclassified -> PokitButtonType.PRIMARY + }, + onClick = { viewModel.updateCategory(Category.Unclassified) } + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + Icon( + modifier = Modifier.size(18.dp), + painter = painterResource(id = DrawableResource.icon_24_align), + contentDescription = null + ) + + Spacer(modifier = Modifier.padding(start = 2.dp)) + + Text( + modifier = Modifier + .clickable { + when (viewModel.sortOrder.value) { + is SortOrder.Latest -> viewModel.updateSortOrder(SortOrder.Name) + is SortOrder.Name -> viewModel.updateSortOrder(SortOrder.Latest) + } + } + .align(Alignment.CenterVertically), + text = when (viewModel.sortOrder.value) { + is SortOrder.Latest -> "최신순" + is SortOrder.Name -> "이름순" + }, + style = PokitTheme.typography.body3Medium + ) + } + } + Spacer(modifier = Modifier.height(20.dp)) + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewCustomRow() { + HomeMid() +} diff --git a/feature/home/src/main/java/pokitmons/pokit/home/pokit/PokitScreen.kt b/feature/home/src/main/java/pokitmons/pokit/home/pokit/PokitScreen.kt new file mode 100644 index 00000000..3377eba5 --- /dev/null +++ b/feature/home/src/main/java/pokitmons/pokit/home/pokit/PokitScreen.kt @@ -0,0 +1,85 @@ +package pokitmons.pokit.home.pokit + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import coil.compose.rememberAsyncImagePainter +import pokitmons.pokit.core.ui.components.block.pokitcard.PokitCard +import pokitmons.pokit.core.ui.components.template.bottomsheet.PokitBottomSheet +import pokitmons.pokit.core.ui.components.template.modifybottomsheet.ModifyBottomSheetContent + +@Composable +fun PokitScreen( + modifier: Modifier = Modifier, + viewModel: PokitViewModel, + onNavigateToPokitDetail: (String) -> Unit, +) { + viewModel.loadPokits() + var showBottomSheet by remember { mutableStateOf(false) } + val pokits = viewModel.pokits.collectAsState() + + Column( + modifier = modifier + .background(color = Color.White) + .padding(horizontal = 20.dp) + .fillMaxSize() + ) { + HomeMid() + + when (viewModel.selectedCategory.value) { + is Category.Pokit -> { + LazyVerticalGrid( + modifier = Modifier.fillMaxSize(), + columns = GridCells.Fixed(2), + contentPadding = PaddingValues(bottom = 100.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(pokits.value) { pokitDetail -> + PokitCard( + text = pokitDetail.title, + linkCount = pokitDetail.count, + painter = rememberAsyncImagePainter(model = pokitDetail.image.url), + onClick = { onNavigateToPokitDetail(pokitDetail.id) }, + onClickKebab = { + showBottomSheet = true + } + ) + } + } + } + + is Category.Unclassified -> { + UnclassifiedScreen() + } + } + + if (showBottomSheet) { + PokitBottomSheet( + onHideBottomSheet = { showBottomSheet = false }, + show = showBottomSheet + ) { + ModifyBottomSheetContent( + onClickShare = { }, + onClickRemove = { }, + onClickModify = { } + ) + } + } + } +} diff --git a/feature/home/src/main/java/pokitmons/pokit/home/pokit/PokitViewModel.kt b/feature/home/src/main/java/pokitmons/pokit/home/pokit/PokitViewModel.kt new file mode 100644 index 00000000..dd191d15 --- /dev/null +++ b/feature/home/src/main/java/pokitmons/pokit/home/pokit/PokitViewModel.kt @@ -0,0 +1,127 @@ +package pokitmons.pokit.home.pokit + +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.strayalpaca.pokitdetail.model.Pokit +import com.strayalpaca.pokitdetail.paging.LinkPaging +import com.strayalpaca.pokitdetail.paging.PokitPaging +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.link.Link +import pokitmons.pokit.domain.model.link.LinksSort +import pokitmons.pokit.domain.usecase.link.GetLinksUseCase +import pokitmons.pokit.domain.usecase.pokit.GetPokitsUseCase +import javax.inject.Inject +import com.strayalpaca.pokitdetail.model.Link as DetailLink + +@HiltViewModel +class PokitViewModel @Inject constructor( + private val getPokitsUseCase: GetPokitsUseCase, + private val getLinksUseCase: GetLinksUseCase, +) : ViewModel() { + + var selectedCategory = mutableStateOf(Category.Pokit) + private set + + var sortOrder = mutableStateOf(SortOrder.Latest) + private set + + var screenType = mutableStateOf(ScreenType.Pokit) + private set + + private val pokitPaging = PokitPaging( + getPokits = getPokitsUseCase, + perPage = 10, + coroutineScope = viewModelScope, + initPage = 0 + ) + + private val linkPaging = LinkPaging( + getLinks = ::getUncategorizedLinks, + perPage = 10, + coroutineScope = viewModelScope, + initPage = 0, + initCategoryId = 1 + ) + + private var _pokits: MutableStateFlow> = pokitPaging._pagingData + val pokits: StateFlow> + get() = _pokits.asStateFlow() + + private var _unCategoryLinks: MutableStateFlow> = linkPaging._pagingData + val unCategoryLinks: StateFlow> + get() = _unCategoryLinks.asStateFlow() + + fun updateCategory(category: Category) { + selectedCategory.value = category + } + + fun updateSortOrder(order: SortOrder) { + sortOrder.value = order + sortPokits() + } + + private fun sortPokits() { + when (sortOrder.value) { + is SortOrder.Name -> { + _pokits.update { pokit -> + pokit.sortedBy { pokitDetail -> + pokitDetail.title + } + } + } + is SortOrder.Latest -> { + _pokits.update { pokit -> + pokit.sortedByDescending { pokitDetail -> + pokitDetail.createdAt + } + } + } + } + } + + private suspend fun getUncategorizedLinks(categoryId: Int, size: Int, page: Int, sort: LinksSort): PokitResult> { + return getLinksUseCase.getUncategorizedLinks( + size = size, + page = page, + sort = sort + ) + } + + fun updateScreenType(type: ScreenType) { + screenType.value = type + } + + fun loadPokits() { + viewModelScope.launch { + pokitPaging.load() + } + } + + fun loadUnCategoryLinks() { + viewModelScope.launch { + linkPaging.load() + } + } +} + +sealed class Category { + data object Pokit : Category() + data object Unclassified : Category() +} + +sealed class SortOrder { + data object Latest : SortOrder() + data object Name : SortOrder() +} + +sealed class ScreenType { + data object Pokit : ScreenType() + data object Remind : ScreenType() +} diff --git a/feature/home/src/main/java/pokitmons/pokit/home/pokit/UnclassifiedScreen.kt b/feature/home/src/main/java/pokitmons/pokit/home/pokit/UnclassifiedScreen.kt new file mode 100644 index 00000000..fbe6904e --- /dev/null +++ b/feature/home/src/main/java/pokitmons/pokit/home/pokit/UnclassifiedScreen.kt @@ -0,0 +1,45 @@ +package pokitmons.pokit.home.pokit + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import coil.compose.rememberAsyncImagePainter +import pokitmons.pokit.core.ui.components.block.linkcard.LinkCard + +@Composable +fun UnclassifiedScreen(viewModel: PokitViewModel = hiltViewModel()) { + viewModel.loadUnCategoryLinks() + val unCategoryLinks = viewModel.unCategoryLinks.collectAsState() + + LazyColumn( + modifier = Modifier, + contentPadding = PaddingValues(bottom = 20.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + items(items = unCategoryLinks.value) { unCategoryDetail -> + LinkCard( + item = unCategoryDetail.linkType, + title = "자연 친화적인 라이프스타일을 위한 환경 보호 방법", + sub = unCategoryDetail.createdAt, + painter = rememberAsyncImagePainter(model = unCategoryDetail.imageUrl), + notRead = !unCategoryDetail.isRead, + badgeText = "미분류", + onClickKebab = { }, + onClickItem = { } + ) + } + } +} + +@Preview +@Composable +fun LinkCardPreview2() { + UnclassifiedScreen() +} diff --git a/feature/home/src/main/java/pokitmons/pokit/home/remind/RemindScreen.kt b/feature/home/src/main/java/pokitmons/pokit/home/remind/RemindScreen.kt new file mode 100644 index 00000000..e9c6e4c1 --- /dev/null +++ b/feature/home/src/main/java/pokitmons/pokit/home/remind/RemindScreen.kt @@ -0,0 +1,102 @@ +package pokitmons.pokit.home.remind + +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import coil.compose.rememberAsyncImagePainter +import pokitmons.pokit.core.ui.components.block.linkcard.LinkCard + +@Composable +fun RemindScreen( + modifier: Modifier = Modifier, + viewModel: RemindViewModel = hiltViewModel(), +) { + val unreadContents = viewModel.unReadContents.collectAsState() + val todayContents = viewModel.todayContents.collectAsState() + val bookmarkContents = viewModel.bookmarkContents.collectAsState() + + Column( + modifier = modifier + .padding(20.dp) + .fillMaxHeight() + .verticalScroll(rememberScrollState()) + ) { + Spacer(modifier = Modifier.height(4.dp)) + + RemindSection(title = "오늘 이 링크는 어때요?") { + Spacer(modifier = Modifier.height(12.dp)) + Row( + modifier = Modifier.horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + todayContents.value.forEach { todayContent -> + ToadyLinkCard( + title = todayContent.title, + sub = todayContent.createdAt, + painter = rememberAsyncImagePainter(todayContent.thumbNail), + badgeText = todayContent.data, + domain = todayContent.domain + ) + } + } + } + + Spacer(modifier = Modifier.height(32.dp)) + + RemindSection(title = "한번도 읽지 않았어요") { + Spacer(modifier = Modifier.height(16.dp)) + Column( + modifier = Modifier, + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + unreadContents.value.forEach { unReadContent -> + LinkCard( + item = unReadContent.title, + title = unReadContent.title, + sub = "${unReadContent.createdAt} • ${unReadContent.domain}", + painter = rememberAsyncImagePainter(unReadContent.thumbNail), + notRead = unReadContent.isRead, + badgeText = unReadContent.data, + onClickKebab = { }, + onClickItem = { } + ) + } + } + } + + Spacer(modifier = Modifier.height(32.dp)) + + RemindSection(title = "즐겨찾기 링크만 모았어요") { + Spacer(modifier = Modifier.height(12.dp)) + Column( + modifier = Modifier, + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + bookmarkContents.value.forEach { unReadContent -> + LinkCard( + item = unReadContent.title, + title = unReadContent.title, + sub = "${unReadContent.createdAt} • ${unReadContent.domain}", + painter = rememberAsyncImagePainter(unReadContent.thumbNail), + notRead = unReadContent.isRead, + badgeText = unReadContent.data, + onClickKebab = { }, + onClickItem = { } + ) + } + } + } + } +} diff --git a/feature/home/src/main/java/pokitmons/pokit/home/remind/RemindSection.kt b/feature/home/src/main/java/pokitmons/pokit/home/remind/RemindSection.kt new file mode 100644 index 00000000..bb353e83 --- /dev/null +++ b/feature/home/src/main/java/pokitmons/pokit/home/remind/RemindSection.kt @@ -0,0 +1,20 @@ +package pokitmons.pokit.home.remind + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import pokitmons.pokit.core.ui.theme.PokitTheme + +@Composable +fun RemindSection( + title: String, + content: @Composable () -> Unit, +) { + Column { + Text( + text = title, + style = PokitTheme.typography.title2 + ) + content() + } +} diff --git a/feature/home/src/main/java/pokitmons/pokit/home/remind/RemindViewModel.kt b/feature/home/src/main/java/pokitmons/pokit/home/remind/RemindViewModel.kt new file mode 100644 index 00000000..bf3deefb --- /dev/null +++ b/feature/home/src/main/java/pokitmons/pokit/home/remind/RemindViewModel.kt @@ -0,0 +1,61 @@ +package pokitmons.pokit.home.remind + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.home.remind.RemindResult +import pokitmons.pokit.domain.usecase.home.remind.BookMarkContentsUseCase +import pokitmons.pokit.domain.usecase.home.remind.TodayContentsUseCase +import pokitmons.pokit.domain.usecase.home.remind.UnReadContentsUseCase +import javax.inject.Inject + +@HiltViewModel +class RemindViewModel @Inject constructor( + private val unReadContentsUseCase: UnReadContentsUseCase, + private val todayContentsUseCase: TodayContentsUseCase, + private val bookMarkContentsUseCase: BookMarkContentsUseCase, +) : ViewModel() { + + init { + loadContents() + } + + private var _unReadContents: MutableStateFlow> = MutableStateFlow(emptyList()) + val unReadContents: StateFlow> + get() = _unReadContents.asStateFlow() + + private var _todayContents: MutableStateFlow> = MutableStateFlow(emptyList()) + val todayContents: StateFlow> + get() = _todayContents.asStateFlow() + + private var _bookmarkContents: MutableStateFlow> = MutableStateFlow(emptyList()) + val bookmarkContents: StateFlow> + get() = _bookmarkContents.asStateFlow() + + fun loadContents() { + viewModelScope.launch { + when (val response = unReadContentsUseCase.getUnreadContents()) { + is PokitResult.Success -> _unReadContents.value = response.result.take(3) + is PokitResult.Error -> {} + } + + when (val response = todayContentsUseCase.getTodayContents()) { + is PokitResult.Success -> { + _todayContents.value = response.result + } + is PokitResult.Error -> { + } + } + + when (val response = bookMarkContentsUseCase.getBookmarkContents()) { + is PokitResult.Success -> _bookmarkContents.value = response.result.take(3) + is PokitResult.Error -> {} + } + } + } +} diff --git a/feature/home/src/main/java/pokitmons/pokit/home/remind/TodayLinkCard.kt b/feature/home/src/main/java/pokitmons/pokit/home/remind/TodayLinkCard.kt new file mode 100644 index 00000000..3c01a522 --- /dev/null +++ b/feature/home/src/main/java/pokitmons/pokit/home/remind/TodayLinkCard.kt @@ -0,0 +1,112 @@ +package pokitmons.pokit.home.remind + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.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.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +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.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import pokitmons.pokit.core.ui.theme.PokitTheme +import pokitmons.pokit.home.R + +@Composable +fun ToadyLinkCard( + viewModel: RemindViewModel = hiltViewModel(), + title: String, + sub: String, + painter: Painter, + badgeText: String, + domain: String, +) { + Box( + modifier = Modifier + .width(216.dp) + .height(194.dp) + .clip(RoundedCornerShape(8.dp)) + ) { + Image( + painter = painter, + contentDescription = null, + modifier = Modifier.fillMaxSize() + ) + Box( + modifier = Modifier + .fillMaxSize() + .background( + Brush.verticalGradient( + colors = listOf(Color.Transparent, Color(0xFF060606)) + ) + ) + ) + Column( + modifier = Modifier + .align(Alignment.BottomStart) + .padding(12.dp) + ) { + Text( + style = PokitTheme.typography.label4, + text = badgeText, + color = PokitTheme.colors.textTertiary, + modifier = Modifier + .height(16.dp) + .width(34.dp) + .clip(RoundedCornerShape(4.dp)) + .background(PokitTheme.colors.backgroundPrimary) + .padding(horizontal = 4.dp, vertical = 2.dp) + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top + ) { + Text( + modifier = Modifier.weight(1f), + style = PokitTheme.typography.body2Bold, + text = title, + color = Color.White, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + + Spacer(modifier = Modifier.padding(4.dp)) + + Icon( + painter = painterResource(id = pokitmons.pokit.core.ui.R.drawable.icon_24_kebab), + contentDescription = null, + tint = Color.White + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "$sub · $domain", + style = PokitTheme.typography.detail2, + color = PokitTheme.colors.textTertiary + ) + } + } +} diff --git a/feature/home/src/main/res/values/strings.xml b/feature/home/src/main/res/values/strings.xml new file mode 100644 index 00000000..19bf5d4e --- /dev/null +++ b/feature/home/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + home + \ No newline at end of file diff --git a/feature/home/src/test/java/pokitmons/pokit/home/ExampleUnitTest.kt b/feature/home/src/test/java/pokitmons/pokit/home/ExampleUnitTest.kt new file mode 100644 index 00000000..13a91fbb --- /dev/null +++ b/feature/home/src/test/java/pokitmons/pokit/home/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package pokitmons.pokit.home + +import junit.framework.TestCase.assertEquals +import org.junit.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/feature/login/src/main/java/pokitmons/pokit/LoginViewModel.kt b/feature/login/src/main/java/pokitmons/pokit/LoginViewModel.kt index 581c9681..2dbd131e 100644 --- a/feature/login/src/main/java/pokitmons/pokit/LoginViewModel.kt +++ b/feature/login/src/main/java/pokitmons/pokit/LoginViewModel.kt @@ -1,35 +1,51 @@ package pokitmons.pokit +import android.content.Context +import androidx.compose.runtime.mutableStateListOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.usecase.auth.InputNicknameUseCase import pokitmons.pokit.domain.usecase.auth.SNSLoginUseCase +import pokitmons.pokit.domain.usecase.auth.SignUpUseCase +import pokitmons.pokit.login.R +import pokitmons.pokit.model.CategoryState +import pokitmons.pokit.model.DuplicateNicknameState import javax.inject.Inject @HiltViewModel class LoginViewModel @Inject constructor( + @ApplicationContext private val context: Context, private val loginUseCase: SNSLoginUseCase, + private val nicknameUseCase: InputNicknameUseCase, + private val signUpUseCase: SignUpUseCase, ) : ViewModel() { - - private var apiRequestJob: Job? = null + private var duplicateNicknameJob: Job? = null private val _loginState: MutableStateFlow = MutableStateFlow(LoginState.Init) val loginState: StateFlow get() = _loginState.asStateFlow() - private val _inputNicknameState = MutableStateFlow("") - val inputNicknameState: StateFlow + private val _inputNicknameState = MutableStateFlow(DuplicateNicknameState()) + val inputNicknameState: StateFlow get() = _inputNicknameState.asStateFlow() - fun inputText(text: String) { - _inputNicknameState.value = text + private val _categories = mutableStateListOf() + val categories: List get() = _categories + + fun inputText(inputNickname: String) { + _inputNicknameState.update { duplicateNicknameState -> + duplicateNicknameState.copy(nickname = inputNickname) + } } fun snsLogin(authPlatform: String, idToken: String) { @@ -50,15 +66,68 @@ class LoginViewModel @Inject constructor( } } + fun signUp() { + viewModelScope.launch { + when ( + val signUpResult = signUpUseCase.signUp( + nickname = _inputNicknameState.value.nickname, + categories = _categories + .filter { category -> category.isSelected.value } + .map { categoryState -> categoryState.name } + ) + ) { + is PokitResult.Success -> { } + is PokitResult.Error -> { } + } + } + } + fun checkDuplicateNickname(nickname: String) { - apiRequestJob?.cancel() - apiRequestJob = viewModelScope.launch { + duplicateNicknameJob?.cancel() + duplicateNicknameJob = viewModelScope.launch { delay(1.second()) - // TOOD api 연동 + when (val duplicateNicknameResult = nicknameUseCase.checkDuplicateNickname(nickname)) { + is PokitResult.Success -> { + _inputNicknameState.update { duplicateNicknameState -> + duplicateNicknameState.copy(isDuplicate = duplicateNicknameResult.result.isDuplicate) + } + } + is PokitResult.Error -> {} + } } } - val categories: ArrayList = arrayListOf() + fun setCategories() { + val categoryNames: List = listOf( + context.getString(R.string.sports_and_leisure), + context.getString(R.string.phrases_and_office), + context.getString(R.string.fashion), + context.getString(R.string.travel), + context.getString(R.string.economy_and_politics), + context.getString(R.string.movies_and_dramas), + context.getString(R.string.restaurants), + context.getString(R.string.interior), + context.getString(R.string.it), + context.getString(R.string.design), + context.getString(R.string.self_development), + context.getString(R.string.humor), + context.getString(R.string.music), + context.getString(R.string.job_info) + ) + + _categories.clear() + _categories.addAll(categoryNames.map { name -> CategoryState(name = name) }) + } + + fun onClickCategoryItem(category: CategoryState) { + if (category.isSelected.value || isLimitSelected()) { + category.isSelected.value = !category.isSelected.value + } + } + + private fun isLimitSelected(): Boolean { + return _categories.count { it.isSelected.value } < LIMIT_SELECTED_COUNT + } var accessToken: String = "" private set @@ -66,13 +135,12 @@ class LoginViewModel @Inject constructor( var refreshToken: String = "" private set - var nickname: String = "" - private set - // TODO 확장함수 모듈 생성하기 companion object { private fun Int.second(): Long { return (this * 1000L) } + + private const val LIMIT_SELECTED_COUNT = 3 } } diff --git a/feature/login/src/main/java/pokitmons/pokit/keyword/KeywordScreen.kt b/feature/login/src/main/java/pokitmons/pokit/keyword/KeywordScreen.kt index 2f3e410f..c2ea7e21 100644 --- a/feature/login/src/main/java/pokitmons/pokit/keyword/KeywordScreen.kt +++ b/feature/login/src/main/java/pokitmons/pokit/keyword/KeywordScreen.kt @@ -1,8 +1,11 @@ package pokitmons.pokit.keyword +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.Row +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -11,6 +14,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -21,16 +25,22 @@ import pokitmons.pokit.core.ui.components.atom.button.PokitButton import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonSize import pokitmons.pokit.core.ui.components.atom.chip.PokitChip import pokitmons.pokit.core.ui.components.atom.chip.attributes.PokitChipSize +import pokitmons.pokit.core.ui.components.atom.chip.attributes.PokitChipState import pokitmons.pokit.core.ui.theme.PokitTheme import pokitmons.pokit.core.ui.R as Ui import pokitmons.pokit.login.R as Login +@OptIn(ExperimentalLayoutApi::class) @Composable fun KeywordScreen( loginViewModel: LoginViewModel, onNavigateToSignUpScreen: () -> Unit, popBackStack: () -> Unit, ) { + LaunchedEffect(Unit) { + loginViewModel.setCategories() + } + Box( modifier = Modifier .fillMaxSize() @@ -38,114 +48,42 @@ fun KeywordScreen( .padding(bottom = 8.dp) ) { Column { - Icon(painter = painterResource(id = Ui.drawable.icon_24_arrow_left), contentDescription = null) + Icon( + modifier = Modifier.clickable { popBackStack() }, + painter = painterResource(id = Ui.drawable.icon_24_arrow_left), + contentDescription = null + ) + Spacer(modifier = Modifier.height(32.dp)) + Text( text = stringResource(id = Login.string.keyword_title), style = PokitTheme.typography.title1 ) + Spacer(modifier = Modifier.height(12.dp)) + Text( text = stringResource(id = Login.string.select_keyword), style = PokitTheme.typography.title3 ) Spacer(modifier = Modifier.height(36.dp)) - // TODO FlowRow도 사용해보기 - Column { - val categories: List> = listOf( - stringResource(id = Login.string.sports_and_leisure), - stringResource(id = Login.string.phrases_and_office), - stringResource(id = Login.string.fashion), - stringResource(id = Login.string.travel), - stringResource(id = Login.string.economy_and_politics), - stringResource(id = Login.string.movies_and_dramas), - stringResource(id = Login.string.restaurants), - stringResource(id = Login.string.interior), - stringResource(id = Login.string.it), - stringResource(id = Login.string.design), - stringResource(id = Login.string.self_development), - stringResource(id = Login.string.humor), - stringResource(id = Login.string.music), - stringResource(id = Login.string.job_info) - ).chunked(3) - - Row { - categories[0].forEach { category -> - PokitChip( - data = null, - size = PokitChipSize.MEDIUM, - text = category, - removeIconPosition = null, - onClickRemove = { }, - onClickItem = { } - ) - Spacer(modifier = Modifier.padding(start = 12.dp)) - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - Row { - categories[1].forEach { category -> - PokitChip( - data = null, - size = PokitChipSize.MEDIUM, - text = category, - removeIconPosition = null, - onClickRemove = { }, - onClickItem = { } - ) - Spacer(modifier = Modifier.padding(start = 12.dp)) - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - Row { - categories[2].forEach { category -> - PokitChip( - data = null, - size = PokitChipSize.MEDIUM, - text = category, - removeIconPosition = null, - onClickRemove = { }, - onClickItem = { } - ) - Spacer(modifier = Modifier.padding(start = 12.dp)) - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - Row { - categories[3].forEach { category -> - PokitChip( - data = null, - size = PokitChipSize.MEDIUM, - text = category, - removeIconPosition = null, - onClickRemove = { }, - onClickItem = { } - ) - Spacer(modifier = Modifier.padding(start = 12.dp)) - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - Row { - categories[4].forEach { category -> - PokitChip( - data = null, - size = PokitChipSize.MEDIUM, - text = category, - removeIconPosition = null, - onClickRemove = { }, - onClickItem = { } - ) - Spacer(modifier = Modifier.padding(start = 12.dp)) - } + FlowRow( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + maxItemsInEachRow = 3 + ) { + loginViewModel.categories.forEachIndexed { index, category -> + PokitChip( + state = if (category.isSelected.value) PokitChipState.FILLED else PokitChipState.DEFAULT, + data = null, + size = PokitChipSize.MEDIUM, + text = category.name, + removeIconPosition = null, + onClickRemove = { }, + onClickItem = { loginViewModel.onClickCategoryItem(category) } + ) } } } @@ -157,7 +95,8 @@ fun KeywordScreen( text = stringResource(id = pokitmons.pokit.login.R.string.next), icon = null, size = PokitButtonSize.LARGE, - onClick = { onNavigateToSignUpScreen() } + onClick = { loginViewModel.signUp() }, + enable = loginViewModel.categories.count { it.isSelected.value } >= 1 ) } } diff --git a/feature/login/src/main/java/pokitmons/pokit/login/LoginScreen.kt b/feature/login/src/main/java/pokitmons/pokit/login/LoginScreen.kt index 23a6e104..0ba58738 100644 --- a/feature/login/src/main/java/pokitmons/pokit/login/LoginScreen.kt +++ b/feature/login/src/main/java/pokitmons/pokit/login/LoginScreen.kt @@ -102,7 +102,7 @@ fun LoginScreen( loginType = PokitLoginButtonType.GOOGLE, text = stringResource(id = R.string.google_login), onClick = { -// onNavigateToTermsOfServiceScreen() + onNavigateToTermsOfServiceScreen() googleLogin( snsLogin = loginViewModel::snsLogin, coroutineScope = coroutineScope, diff --git a/feature/login/src/main/java/pokitmons/pokit/model/CategoryState.kt b/feature/login/src/main/java/pokitmons/pokit/model/CategoryState.kt new file mode 100644 index 00000000..c50dc20f --- /dev/null +++ b/feature/login/src/main/java/pokitmons/pokit/model/CategoryState.kt @@ -0,0 +1,9 @@ +package pokitmons.pokit.model + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf + +data class CategoryState( + val name: String, + var isSelected: MutableState = mutableStateOf(false), +) diff --git a/feature/login/src/main/java/pokitmons/pokit/model/DuplicateNicknameState.kt b/feature/login/src/main/java/pokitmons/pokit/model/DuplicateNicknameState.kt new file mode 100644 index 00000000..62086325 --- /dev/null +++ b/feature/login/src/main/java/pokitmons/pokit/model/DuplicateNicknameState.kt @@ -0,0 +1,6 @@ +package pokitmons.pokit.model + +data class DuplicateNicknameState( + val nickname: String = "", + val isDuplicate: Boolean = true, +) diff --git a/feature/login/src/main/java/pokitmons/pokit/nickname/InputNicknameScreen.kt b/feature/login/src/main/java/pokitmons/pokit/nickname/InputNicknameScreen.kt index eba8de5c..4526b335 100644 --- a/feature/login/src/main/java/pokitmons/pokit/nickname/InputNicknameScreen.kt +++ b/feature/login/src/main/java/pokitmons/pokit/nickname/InputNicknameScreen.kt @@ -62,18 +62,21 @@ fun InputNicknameScreen( .fillMaxWidth() .imePadding(), label = "", - inputText = inputNicknameState, + inputText = inputNicknameState.nickname, maxLength = NICKNAME_MAX_LENGTH, - sub = if (inputNicknameState.length < NICKNAME_MAX_LENGTH) { - stringResource(id = Login.string.input_restriction_message) - } else { - stringResource(id = Login.string.input_max_length) + sub = when { + inputNicknameState.nickname.length < NICKNAME_MAX_LENGTH -> stringResource(id = Login.string.input_restriction_message) + !inputNicknameState.isDuplicate -> stringResource(id = Login.string.nickname_already_in_use) + else -> stringResource(id = Login.string.input_max_length) }, - isError = inputNicknameState.length > NICKNAME_MAX_LENGTH, + isError = inputNicknameState.nickname.length > NICKNAME_MAX_LENGTH || !inputNicknameState.isDuplicate, hintText = stringResource(id = Login.string.input_nickname_hint), onChangeText = { text -> if (text.length <= NICKNAME_MAX_LENGTH) { - loginViewModel.inputText(text) + loginViewModel.apply { + inputText(text) + checkDuplicateNickname(text) + } } } ) @@ -86,7 +89,7 @@ fun InputNicknameScreen( text = stringResource(id = pokitmons.pokit.login.R.string.next), icon = null, size = PokitButtonSize.LARGE, - enable = inputNicknameState.length >= NICKNAME_MIN_LENGTH, + enable = !inputNicknameState.isDuplicate, onClick = { onNavigateToKeywordScreen() } ) } diff --git a/feature/login/src/main/res/values/strings.xml b/feature/login/src/main/res/values/strings.xml index f401ce81..0012ad0b 100644 --- a/feature/login/src/main/res/values/strings.xml +++ b/feature/login/src/main/res/values/strings.xml @@ -9,6 +9,7 @@ Pokit에 사용할 닉네임을\n입력해주세요 내용을 입력해주세요. 한글, 영어, 숫자로만 입력이 가능합니다. + 사용 중인 닉네임입니다. 최대 20자까지 입력 가능합니다. 어떤 분야에 관심이 있으세요? 최대 3개를 골라주시면,\n관련 콘텐츠를 추천해드릴게요! diff --git a/feature/pokitdetail/build.gradle.kts b/feature/pokitdetail/build.gradle.kts index ad7fc721..76cf12f3 100644 --- a/feature/pokitdetail/build.gradle.kts +++ b/feature/pokitdetail/build.gradle.kts @@ -1,6 +1,8 @@ plugins { alias(libs.plugins.com.android.library) alias(libs.plugins.org.jetbrains.kotlin.android) + alias(libs.plugins.hilt) + id("kotlin-kapt") } android { @@ -60,5 +62,10 @@ dependencies { implementation(libs.orbit.core) implementation(libs.orbit.viewmodel) + // hilt + implementation(libs.hilt) + kapt(libs.hilt.compiler) + implementation(project(":core:ui")) + implementation(project(":domain")) } diff --git a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/PokitDetailScreen.kt b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/PokitDetailScreen.kt index 1264b5ad..d8788948 100644 --- a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/PokitDetailScreen.kt +++ b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/PokitDetailScreen.kt @@ -8,10 +8,14 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -25,6 +29,7 @@ import com.strayalpaca.pokitdetail.model.Filter import com.strayalpaca.pokitdetail.model.Link import com.strayalpaca.pokitdetail.model.Pokit import com.strayalpaca.pokitdetail.model.PokitDetailScreenState +import com.strayalpaca.pokitdetail.paging.SimplePagingState import pokitmons.pokit.core.ui.components.block.linkcard.LinkCard import pokitmons.pokit.core.ui.components.block.pokitlist.PokitList import pokitmons.pokit.core.ui.components.block.pokitlist.attributes.PokitListState @@ -38,10 +43,14 @@ import pokitmons.pokit.core.ui.R.drawable as coreDrawable fun PokitDetailScreenContainer( viewModel: PokitDetailViewModel, onBackPressed: () -> Unit, + onNavigateToLinkModify: (String) -> Unit, + onNavigateToPokitModify: (String) -> Unit, ) { val state by viewModel.state.collectAsState() val linkList by viewModel.linkList.collectAsState() + val linkListState by viewModel.linkListState.collectAsState() val pokitList by viewModel.pokitList.collectAsState() + val pokitListState by viewModel.pokitListState.collectAsState() PokitDetailScreen( onBackPressed = onBackPressed, @@ -60,8 +69,16 @@ fun PokitDetailScreenContainer( hideLinkDetailBottomSheet = viewModel::hideLinkDetailBottomSheet, state = state, linkList = linkList, + linkListState = linkListState, pokitList = pokitList, - onClickLink = viewModel::showLinkDetailBottomSheet + pokitListState = pokitListState, + onClickLink = viewModel::showLinkDetailBottomSheet, + onClickPokitModify = onNavigateToPokitModify, + onClickPokitRemove = viewModel::deletePokit, + onClickLinkModify = onNavigateToLinkModify, + loadNextPokits = viewModel::loadNextPokits, + refreshPokits = viewModel::refreshPokits, + loadNextLinks = viewModel::loadNextLinks ) } @@ -83,8 +100,16 @@ fun PokitDetailScreen( hideLinkDetailBottomSheet: () -> Unit = {}, state: PokitDetailScreenState = PokitDetailScreenState(), linkList: List = emptyList(), + linkListState: SimplePagingState = SimplePagingState.IDLE, pokitList: List = emptyList(), + pokitListState: SimplePagingState = SimplePagingState.IDLE, onClickLink: (Link) -> Unit = {}, + onClickPokitModify: (String) -> Unit = {}, + onClickPokitRemove: () -> Unit = {}, + onClickLinkModify: (String) -> Unit = {}, + loadNextPokits: () -> Unit = {}, + refreshPokits: () -> Unit = {}, + loadNextLinks: () -> Unit = {}, ) { Column( modifier = Modifier.fillMaxSize() @@ -97,16 +122,32 @@ fun PokitDetailScreen( Spacer(modifier = Modifier.height(12.dp)) TitleArea( - title = state.currentPokit.title, - sub = stringResource(id = pokitmons.pokit.core.ui.R.string.pokit_count_format, state.currentPokit.count), + title = state.currentPokit?.title ?: "", + sub = stringResource(id = pokitmons.pokit.core.ui.R.string.pokit_count_format, state.currentPokit?.count ?: 0), onClickSelectPokit = showPokitSelectBottomSheet, onClickSelectFilter = onClickFilter ) + val linkLazyColumnListState = rememberLazyListState() + val startLinkPaging = remember { + derivedStateOf { + linkLazyColumnListState.layoutInfo.visibleItemsInfo.lastOrNull()?.let { last -> + last.index >= linkLazyColumnListState.layoutInfo.totalItemsCount - 3 + } ?: false + } + } + + LaunchedEffect(startLinkPaging.value) { + if (startLinkPaging.value && linkListState == SimplePagingState.IDLE) { + loadNextLinks() + } + } + LazyColumn( modifier = Modifier .fillMaxWidth() - .weight(1f) + .weight(1f), + state = linkLazyColumnListState ) { items(linkList) { link -> LinkCard( @@ -146,7 +187,28 @@ fun PokitDetailScreen( onHideBottomSheet = hidePokitSelectBottomSheet, show = state.pokitSelectBottomSheetVisible ) { - LazyColumn { + val lazyColumnListState = rememberLazyListState() + val startPaging = remember { + derivedStateOf { + lazyColumnListState.layoutInfo.visibleItemsInfo.lastOrNull()?.let { last -> + last.index >= lazyColumnListState.layoutInfo.totalItemsCount - 3 + } ?: false + } + } + + LaunchedEffect(Unit) { + refreshPokits() + } + + LaunchedEffect(startPaging.value) { + if (startPaging.value && pokitListState == SimplePagingState.IDLE) { + loadNextPokits() + } + } + + LazyColumn( + state = lazyColumnListState + ) { items( items = pokitList ) { pokit -> @@ -169,7 +231,14 @@ fun PokitDetailScreen( BottomSheetType.MODIFY -> { ModifyBottomSheetContent( onClickShare = {}, - onClickModify = {}, + onClickModify = remember { + { + state.currentLink?.let { link -> + hideLinkModifyBottomSheet() + onClickLinkModify(link.id) + } + } + }, onClickRemove = showLinkRemoveBottomSheet ) } @@ -195,7 +264,12 @@ fun PokitDetailScreen( BottomSheetType.MODIFY -> { ModifyBottomSheetContent( onClickShare = {}, - onClickModify = {}, + onClickModify = remember { + { + hidePokitModifyBottomSheet() + onClickPokitModify(state.currentPokit!!.id) + } + }, onClickRemove = showPokitRemoveBottomSheet ) } @@ -205,7 +279,12 @@ fun PokitDetailScreen( title = stringResource(id = R.string.title_remove_pokit), subText = stringResource(id = R.string.sub_remove_pokit), onClickLeftButton = hidePokitModifyBottomSheet, - onClickRightButton = {} + onClickRightButton = remember { + { + hidePokitModifyBottomSheet() + onClickPokitRemove() + } + } ) } diff --git a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/PokitDetailViewModel.kt b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/PokitDetailViewModel.kt index 4284774f..f241aa06 100644 --- a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/PokitDetailViewModel.kt +++ b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/PokitDetailViewModel.kt @@ -1,37 +1,117 @@ package com.strayalpaca.pokitdetail +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.strayalpaca.pokitdetail.model.BottomSheetType import com.strayalpaca.pokitdetail.model.Filter import com.strayalpaca.pokitdetail.model.Link import com.strayalpaca.pokitdetail.model.Pokit import com.strayalpaca.pokitdetail.model.PokitDetailScreenState -import com.strayalpaca.pokitdetail.model.sampleLinkList -import com.strayalpaca.pokitdetail.model.samplePokitList +import com.strayalpaca.pokitdetail.paging.LinkPaging +import com.strayalpaca.pokitdetail.paging.PokitPaging +import com.strayalpaca.pokitdetail.paging.SimplePagingState +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.link.LinksSort +import pokitmons.pokit.domain.usecase.link.GetLinksUseCase +import pokitmons.pokit.domain.usecase.pokit.DeletePokitUseCase +import pokitmons.pokit.domain.usecase.pokit.GetPokitUseCase +import pokitmons.pokit.domain.usecase.pokit.GetPokitsUseCase +import javax.inject.Inject +import pokitmons.pokit.domain.model.link.Link as DomainLink + +@HiltViewModel +class PokitDetailViewModel @Inject constructor( + private val getPokitsUseCase: GetPokitsUseCase, + private val getLinksUseCase: GetLinksUseCase, + private val getPokitUseCase: GetPokitUseCase, + private val deletePokitUseCase: DeletePokitUseCase, + savedStateHandle: SavedStateHandle, +) : ViewModel() { + private val pokitPaging = PokitPaging( + getPokits = getPokitsUseCase, + perPage = 10, + coroutineScope = viewModelScope, + initPage = 0 + ) + + private val linkPaging = LinkPaging( + getLinks = ::getLinks, + perPage = 10, + coroutineScope = viewModelScope, + initPage = 0, + initCategoryId = 1 + ) -class PokitDetailViewModel : ViewModel() { private val _state = MutableStateFlow(PokitDetailScreenState()) val state: StateFlow = _state.asStateFlow() - private val _pokitList = MutableStateFlow(samplePokitList) - val pokitList: StateFlow> = _pokitList.asStateFlow() + val pokitList: StateFlow> = pokitPaging.pagingData + val pokitListState: StateFlow = pokitPaging.pagingState - private val _linkList = MutableStateFlow(sampleLinkList) - val linkList: StateFlow> = _linkList.asStateFlow() + val linkList: StateFlow> = linkPaging.pagingData + val linkListState: StateFlow = linkPaging.pagingState + + init { + savedStateHandle.get("pokit_id")?.toIntOrNull()?.let { pokitId -> + linkPaging.changeOptions(categoryId = pokitId, sort = LinksSort.RECENT) + viewModelScope.launch { + linkPaging.refresh() + } + getPokit(pokitId) + } + } + + private suspend fun getLinks(categoryId: Int, size: Int, page: Int, sort: LinksSort): PokitResult> { + val currentFilter = state.value.currentFilter + return getLinksUseCase.getLinks( + categoryId = categoryId, + size = size, + page = page, + sort = sort, + isRead = !currentFilter.notReadChecked, + favorite = currentFilter.bookmarkChecked + ) + } + + fun getPokit(pokitId: Int) { + viewModelScope.launch { + val response = getPokitUseCase.getPokit(pokitId) + if (response is PokitResult.Success) { + _state.update { it.copy(currentPokit = Pokit.fromDomainPokit(response.result)) } + } + } + } fun changePokit(pokit: Pokit) { _state.update { it.copy(currentPokit = pokit, pokitSelectBottomSheetVisible = false) } + linkPaging.changeOptions(categoryId = pokit.id.toInt(), sort = LinksSort.RECENT) + viewModelScope.launch { + linkPaging.refresh() + } } fun changeFilter(filter: Filter) { + val currentFilter = state.value.currentFilter + if (currentFilter == filter) { + _state.update { it.copy(filterChangeBottomSheetVisible = false) } + return + } + _state.update { it.copy(currentFilter = filter, filterChangeBottomSheetVisible = false) } + viewModelScope.launch { + linkPaging.refresh() + } } fun showPokitModifyBottomSheet() { + state.value.currentPokit ?: return _state.update { it.copy(pokitBottomSheetType = BottomSheetType.MODIFY) } } @@ -78,4 +158,33 @@ class PokitDetailViewModel : ViewModel() { fun hidePokitSelectBottomSheet() { _state.update { it.copy(pokitSelectBottomSheetVisible = false) } } + + fun loadNextPokits() { + viewModelScope.launch { + pokitPaging.load() + } + } + + fun refreshPokits() { + viewModelScope.launch { + pokitPaging.refresh() + } + } + + fun loadNextLinks() { + viewModelScope.launch { + linkPaging.load() + } + } + + fun deletePokit() { + val currentPokit = state.value.currentPokit ?: return + val pokitId = currentPokit.id.toInt() + viewModelScope.launch { + val response = deletePokitUseCase.deletePokit(pokitId) + if (response is PokitResult.Success) { + // 뒤로가기? + } + } + } } diff --git a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/components/template/filterselectbottomsheet/FilterSelectBottomSheet.kt b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/components/template/filterselectbottomsheet/FilterSelectBottomSheet.kt index faf3199a..aefffb2e 100644 --- a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/components/template/filterselectbottomsheet/FilterSelectBottomSheet.kt +++ b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/components/template/filterselectbottomsheet/FilterSelectBottomSheet.kt @@ -91,7 +91,7 @@ internal fun FilterSelectBottomSheet( ) RadioText( selected = !currentFilter.recentSortUsed, - title = stringResource(id = R.string.sort_recent), + title = stringResource(id = R.string.sort_old), onClick = remember { { currentFilter = currentFilter.copy(recentSortUsed = false) } }, modifier = Modifier.height(40.dp) ) diff --git a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/model/Link.kt b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/model/Link.kt index dac79178..d29f99ae 100644 --- a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/model/Link.kt +++ b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/model/Link.kt @@ -1,6 +1,7 @@ package com.strayalpaca.pokitdetail.model import com.strayalpaca.pokitdetail.R +import pokitmons.pokit.domain.model.link.Link as DomainLink data class Link( val id: String = "", @@ -13,7 +14,24 @@ data class Link( val memo: String = "", val bookmark: Boolean = false, val imageUrl: String? = null, -) + val createdAt: String = "", +) { + companion object { + fun fromDomainLink(domainLink: DomainLink): Link { + return Link( + id = domainLink.id.toString(), + title = domainLink.title, + dateString = domainLink.createdAt, + domainUrl = domainLink.domain, + isRead = domainLink.isRead, + url = domainLink.data, + memo = domainLink.memo, + imageUrl = domainLink.thumbnail, + createdAt = domainLink.createdAt + ) + } + } +} enum class LinkType(val textResourceId: Int) { TEXT(R.string.badge_text), diff --git a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/model/Pokit.kt b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/model/Pokit.kt index 0f31edf6..47806ad3 100644 --- a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/model/Pokit.kt +++ b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/model/Pokit.kt @@ -1,15 +1,31 @@ package com.strayalpaca.pokitdetail.model +import pokitmons.pokit.domain.model.pokit.Pokit as DomainPokit + data class Pokit( val title: String = "", val id: String = "", val count: Int = 0, -) + val image: pokitmons.pokit.domain.model.pokit.Pokit.Image, + val createdAt: String, +) { + companion object { + fun fromDomainPokit(pokit: DomainPokit): Pokit { + return Pokit( + title = pokit.name, + id = pokit.categoryId.toString(), + count = pokit.linkCount, + image = pokit.image, + createdAt = pokit.createdAt + ) + } + } +} -internal val samplePokitList = listOf( - Pokit(title = "안드로이드", id = "1", count = 2), - Pokit(title = "IOS", id = "2", count = 2), - Pokit(title = "디자인", id = "3", count = 2), - Pokit(title = "PM", id = "4", count = 1), - Pokit(title = "서버", id = "5", count = 2) -) +// internal val samplePokitList = listOf( +// Pokit(title = "안드로이드", id = "1", count = 2), +// Pokit(title = "IOS", id = "2", count = 2), +// Pokit(title = "디자인", id = "3", count = 2), +// Pokit(title = "PM", id = "4", count = 1), +// Pokit(title = "서버", id = "5", count = 2) +// ) diff --git a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/model/PokitDetailScreenState.kt b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/model/PokitDetailScreenState.kt index f4b5d49f..03b84f98 100644 --- a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/model/PokitDetailScreenState.kt +++ b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/model/PokitDetailScreenState.kt @@ -1,7 +1,7 @@ package com.strayalpaca.pokitdetail.model data class PokitDetailScreenState( - val currentPokit: Pokit = Pokit(), + val currentPokit: Pokit? = null, val currentFilter: Filter = Filter(), val currentLink: Link? = null, val linkBottomSheetType: BottomSheetType? = null, diff --git a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/LinkPaging.kt b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/LinkPaging.kt new file mode 100644 index 00000000..37662455 --- /dev/null +++ b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/LinkPaging.kt @@ -0,0 +1,136 @@ +package com.strayalpaca.pokitdetail.paging + +import com.strayalpaca.pokitdetail.model.Link +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.link.LinksSort +import kotlin.reflect.KSuspendFunction4 +import pokitmons.pokit.domain.model.link.Link as DomainLink + +class LinkPaging( + private var getLinks: KSuspendFunction4>>, + private val perPage: Int = 10, + private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO), + private val initPage: Int = 0, + private val firstRequestPage: Int = 3, + initCategoryId: Int = 0, +) : SimplePaging { + private val _pagingState = MutableStateFlow(SimplePagingState.IDLE) + override val pagingState: StateFlow = _pagingState.asStateFlow() + + val _pagingData: MutableStateFlow> = MutableStateFlow(emptyList()) + + override val pagingData: StateFlow> = _pagingData.asStateFlow() + private var currentPageIndex = initPage + private var requestJob: Job? = null + + private var currentCategoryId: Int = initCategoryId + private var currentSort = LinksSort.RECENT + + fun changeOptions(categoryId: Int, sort: LinksSort) { + currentCategoryId = categoryId + currentSort = sort + } + + override suspend fun refresh() { + requestJob?.cancel() + + _pagingData.update { emptyList() } + _pagingState.update { SimplePagingState.LOADING_INIT } + requestJob = coroutineScope.launch { + try { + currentPageIndex = initPage + val response = getLinks(currentCategoryId, perPage * firstRequestPage, currentPageIndex, currentSort) + when (response) { + is PokitResult.Success -> { + val links = response.result.map { domainLink -> + Link.fromDomainLink(domainLink) + } + applyResponse(links, firstRequestPage) + } + + is PokitResult.Error -> { + _pagingState.update { SimplePagingState.FAILURE_INIT } + } + } + } catch (exception: Exception) { + if (exception !is CancellationException) { + _pagingState.update { SimplePagingState.FAILURE_INIT } + } + } + } + } + + override suspend fun load() { + if (pagingState.value != SimplePagingState.IDLE) return + + requestJob?.cancel() + _pagingState.update { SimplePagingState.LOADING_NEXT } + + requestJob = coroutineScope.launch { + try { + val response = getLinks(currentCategoryId, perPage, currentPageIndex, currentSort) + when (response) { + is PokitResult.Success -> { + val links = response.result.map { domainLink -> + Link.fromDomainLink(domainLink) + } + applyResponse(links) + } + + is PokitResult.Error -> { + _pagingState.update { SimplePagingState.FAILURE_NEXT } + } + } + } catch (exception: Exception) { + if (exception !is CancellationException) { + _pagingState.update { SimplePagingState.FAILURE_NEXT } + } + } + } + } + + private fun applyResponse(dataInResponse: List, multiple: Int = 1) { + if (dataInResponse.size < perPage * multiple) { + _pagingState.update { SimplePagingState.LAST } + } else { + _pagingState.update { SimplePagingState.IDLE } + } + _pagingData.update { _pagingData.value + dataInResponse } + currentPageIndex += multiple + } + + override fun clear() { + requestJob?.cancel() + _pagingData.update { emptyList() } + _pagingState.update { SimplePagingState.IDLE } + } + + override suspend fun deleteItem(item: Link) { + val capturedDataList = _pagingData.value + _pagingData.update { capturedDataList.filter { it.id != item.id } } + } + + override suspend fun modifyItem(item: Link) { + val capturedDataList = _pagingData.value + val targetPokit = capturedDataList.find { it.id == item.id } ?: return + + _pagingData.update { + capturedDataList.map { pokit -> + if (targetPokit.id == pokit.id) { + item + } else { + pokit + } + } + } + } +} diff --git a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/PokitPaging.kt b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/PokitPaging.kt new file mode 100644 index 00000000..ab2229ce --- /dev/null +++ b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/PokitPaging.kt @@ -0,0 +1,125 @@ +package com.strayalpaca.pokitdetail.paging + +import com.strayalpaca.pokitdetail.model.Pokit +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.usecase.pokit.GetPokitsUseCase + +class PokitPaging( + private val getPokits: GetPokitsUseCase, + private val perPage: Int = 10, + private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO), + private val initPage: Int = 0, + private val firstRequestPage: Int = 3, +) : SimplePaging { + private val _pagingState = MutableStateFlow(SimplePagingState.IDLE) + override val pagingState: StateFlow = _pagingState.asStateFlow() + + val _pagingData: MutableStateFlow> = MutableStateFlow(emptyList()) + + override val pagingData: StateFlow> = _pagingData.asStateFlow() + private var currentPageIndex = initPage + private var requestJob: Job? = null + + override suspend fun refresh() { + requestJob?.cancel() + + _pagingData.update { emptyList() } + _pagingState.update { SimplePagingState.LOADING_INIT } + requestJob = coroutineScope.launch { + try { + currentPageIndex = initPage + val response = getPokits.getPokits(size = perPage * firstRequestPage, page = currentPageIndex) + when (response) { + is PokitResult.Success -> { + val pokitList = response.result.map { domainPokit -> + Pokit.fromDomainPokit(domainPokit) + } + applyResponse(pokitList, firstRequestPage) + } + + is PokitResult.Error -> { + _pagingState.update { SimplePagingState.FAILURE_INIT } + } + } + } catch (exception: Exception) { + if (exception !is CancellationException) { + _pagingState.update { SimplePagingState.FAILURE_INIT } + } + } + } + } + + override suspend fun load() { + if (pagingState.value != SimplePagingState.IDLE) return + + requestJob?.cancel() + _pagingState.update { SimplePagingState.LOADING_NEXT } + + requestJob = coroutineScope.launch { + try { + val response = getPokits.getPokits(size = perPage, page = currentPageIndex) + when (response) { + is PokitResult.Success -> { + val pokitList = response.result.map { domainPokit -> + Pokit.fromDomainPokit(domainPokit) + } + applyResponse(pokitList) + } + + is PokitResult.Error -> { + _pagingState.update { SimplePagingState.FAILURE_NEXT } + } + } + } catch (exception: Exception) { + if (exception !is CancellationException) { + _pagingState.update { SimplePagingState.FAILURE_NEXT } + } + } + } + } + + private fun applyResponse(dataInResponse: List, multiple: Int = 1) { + if (dataInResponse.size < perPage * multiple) { + _pagingState.update { SimplePagingState.LAST } + } else { + _pagingState.update { SimplePagingState.IDLE } + } + _pagingData.update { _pagingData.value + dataInResponse } + currentPageIndex += multiple + } + + override fun clear() { + requestJob?.cancel() + _pagingData.update { emptyList() } + _pagingState.update { SimplePagingState.IDLE } + } + + override suspend fun deleteItem(item: Pokit) { + val capturedDataList = _pagingData.value + _pagingData.update { capturedDataList.filter { it.id != item.id } } + } + + override suspend fun modifyItem(item: Pokit) { + val capturedDataList = _pagingData.value + val targetPokit = capturedDataList.find { it.id == item.id } ?: return + + _pagingData.update { + capturedDataList.map { pokit -> + if (targetPokit.id == pokit.id) { + item + } else { + pokit + } + } + } + } +} diff --git a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/SimplePaging.kt b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/SimplePaging.kt new file mode 100644 index 00000000..de38d66e --- /dev/null +++ b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/SimplePaging.kt @@ -0,0 +1,13 @@ +package com.strayalpaca.pokitdetail.paging + +import kotlinx.coroutines.flow.Flow + +interface SimplePaging { + val pagingData: Flow> + suspend fun refresh() + suspend fun load() + val pagingState: Flow + suspend fun modifyItem(item: T) + suspend fun deleteItem(item: T) + fun clear() +} diff --git a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/SimplePagingState.kt b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/SimplePagingState.kt new file mode 100644 index 00000000..a0fc6519 --- /dev/null +++ b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/paging/SimplePagingState.kt @@ -0,0 +1,5 @@ +package com.strayalpaca.pokitdetail.paging + +enum class SimplePagingState { + IDLE, LOADING_NEXT, LOADING_INIT, FAILURE_NEXT, FAILURE_INIT, LAST +} diff --git a/feature/search/build.gradle.kts b/feature/search/build.gradle.kts index ef9a8247..e18eb426 100644 --- a/feature/search/build.gradle.kts +++ b/feature/search/build.gradle.kts @@ -1,6 +1,8 @@ plugins { alias(libs.plugins.com.android.library) alias(libs.plugins.org.jetbrains.kotlin.android) + alias(libs.plugins.hilt) + id("kotlin-kapt") } android { @@ -60,5 +62,10 @@ dependencies { implementation(libs.orbit.core) implementation(libs.orbit.viewmodel) + // hilt + implementation(libs.hilt) + kapt(libs.hilt.compiler) + implementation(project(":core:ui")) + implementation(project(":domain")) } diff --git a/feature/search/src/main/java/pokitmons/pokit/search/Preview.kt b/feature/search/src/main/java/pokitmons/pokit/search/Preview.kt index d549c85f..b3a10829 100644 --- a/feature/search/src/main/java/pokitmons/pokit/search/Preview.kt +++ b/feature/search/src/main/java/pokitmons/pokit/search/Preview.kt @@ -6,6 +6,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import pokitmons.pokit.core.ui.theme.PokitTheme +import pokitmons.pokit.search.model.Link +import pokitmons.pokit.search.model.LinkType @Preview(showBackground = true) @Composable @@ -14,7 +16,72 @@ private fun Preview() { Column( modifier = Modifier.fillMaxSize() ) { - SearchScreen() + SearchScreen( + linkList = sampleLinks + ) } } } + +internal val sampleLinks = listOf( + Link( + id = "1", + title = "자연 친화적인 라이프스타일을 위한 환경 보호 방법", + imageUrl = null, + dateString = "2024.04.12", + domainUrl = "youtu.be", + isRead = true, + linkType = LinkType.TEXT, + url = "", + memo = "", + bookmark = true + ), + Link( + id = "2", + title = "자연 친화적인 라이프스타일을 위한 환경 보호 방법", + imageUrl = null, + dateString = "2024.05.12", + domainUrl = "youtu.be", + isRead = false, + linkType = LinkType.TEXT, + url = "", + memo = "", + bookmark = true + ), + Link( + id = "3", + title = "포킷포킷", + imageUrl = null, + dateString = "2024.04.12", + domainUrl = "pokitmons.pokit", + isRead = true, + linkType = LinkType.TEXT, + url = "", + memo = "", + bookmark = true + ), + Link( + id = "4", + title = "자연 친화적인 라이프스타일을 위한 환경 보호 방법", + imageUrl = null, + dateString = "2024.06.12", + domainUrl = "youtu.be", + isRead = true, + linkType = LinkType.TEXT, + url = "", + memo = "", + bookmark = true + ), + Link( + id = "5", + title = "마지막 링크입니다.", + imageUrl = null, + dateString = "2024.07.14", + domainUrl = "youtu.be", + isRead = false, + linkType = LinkType.TEXT, + url = "", + memo = "", + bookmark = true + ) +) diff --git a/feature/search/src/main/java/pokitmons/pokit/search/SearchScreen.kt b/feature/search/src/main/java/pokitmons/pokit/search/SearchScreen.kt index 545e0dec..042b733a 100644 --- a/feature/search/src/main/java/pokitmons/pokit/search/SearchScreen.kt +++ b/feature/search/src/main/java/pokitmons/pokit/search/SearchScreen.kt @@ -1,5 +1,6 @@ package pokitmons.pokit.search +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -9,32 +10,108 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.components.template.bottomsheet.PokitBottomSheet +import pokitmons.pokit.core.ui.components.template.modifybottomsheet.ModifyBottomSheetContent +import pokitmons.pokit.core.ui.components.template.removeItemBottomSheet.TwoButtonBottomSheetContent import pokitmons.pokit.core.ui.theme.PokitTheme import pokitmons.pokit.search.components.filter.FilterArea import pokitmons.pokit.search.components.filterbottomsheet.FilterBottomSheet +import pokitmons.pokit.search.components.linkdetailbottomsheet.LinkDetailBottomSheet import pokitmons.pokit.search.components.recentsearchword.RecentSearchWord import pokitmons.pokit.search.components.searchitemlist.SearchItemList import pokitmons.pokit.search.components.toolbar.Toolbar +import pokitmons.pokit.search.model.BottomSheetType import pokitmons.pokit.search.model.Filter import pokitmons.pokit.search.model.FilterType import pokitmons.pokit.search.model.Link import pokitmons.pokit.search.model.SearchScreenState import pokitmons.pokit.search.model.SearchScreenStep +import pokitmons.pokit.search.paging.SimplePagingState @Composable fun SearchScreenContainer( viewModel: SearchViewModel, onBackPressed: () -> Unit, + onNavigateToLinkModify: (String) -> Unit, ) { val state by viewModel.state.collectAsState() val searchWord by viewModel.searchWord.collectAsState() val linkList by viewModel.linkList.collectAsState() + val linkPagingState by viewModel.linkPagingState.collectAsState() + val pokitList by viewModel.pokitList.collectAsState() + val pokitPagingState by viewModel.pokitPagingState.collectAsState() + + LinkDetailBottomSheet( + link = state.currentLink ?: Link(), + onHideBottomSheet = viewModel::hideLinkDetailBottomSheet, + show = state.showLinkDetailBottomSheet, + onClickModifyLink = remember { + { link -> + viewModel.hideLinkDetailBottomSheet() + onNavigateToLinkModify(link.id) + } + }, + onClickRemoveLink = viewModel::showLinkRemoveBottomSheet, + onClickBookmark = viewModel::toggleBookmark + ) + + FilterBottomSheet( + filter = state.filter ?: Filter(), + firstShowType = state.firstBottomSheetFilterType, + show = state.showFilterBottomSheet, + onDismissRequest = viewModel::hideFilterBottomSheet, + onSaveClilck = viewModel::setFilter, + pokits = pokitList, + pokitPagingState = pokitPagingState, + loadNextPokits = viewModel::loadNextPokits, + refreshPokits = viewModel::refreshPokits + ) + + PokitBottomSheet( + onHideBottomSheet = viewModel::hideLinkModifyBottomSheet, + show = state.linkBottomSheetType != null + ) { + if (state.linkBottomSheetType == BottomSheetType.MODIFY) { + ModifyBottomSheetContent( + onClickModify = remember { + { + state.currentLink?.let { link -> + viewModel.hideLinkModifyBottomSheet() + onNavigateToLinkModify(link.id) + } + } + }, + onClickRemove = remember { + { + state.currentLink?.let { link -> + viewModel.showLinkRemoveBottomSheet(link) + } + } + }, + onClickShare = remember { + { + } + } + ) + } + + if (state.linkBottomSheetType == BottomSheetType.REMOVE) { + TwoButtonBottomSheetContent( + title = stringResource(id = R.string.title_remove_link), + subText = stringResource(id = R.string.sub_remove_link), + onClickLeftButton = viewModel::hideLinkModifyBottomSheet, + onClickRightButton = {} + ) + } + } SearchScreen( state = state, currentSearchWord = searchWord, linkList = linkList, + linkPagingState = linkPagingState, onClickBack = onBackPressed, inputSearchWord = viewModel::inputSearchWord, onClickSearch = viewModel::applyCurrentSearchWord, @@ -44,9 +121,10 @@ fun SearchScreenContainer( onClickRemoveRecentSearchWord = viewModel::removeRecentSearchWord, onClickFilterSelect = viewModel::showFilterBottomSheet, onClickFilterItem = viewModel::showFilterBottomSheetWithType, - hideBottomSheet = viewModel::hideFilterBottomSheet, - onClickFilterSave = viewModel::setFilter, - toggleSortOrder = viewModel::toggleSortOrder + toggleSortOrder = viewModel::toggleSortOrder, + showLinkModifyBottomSheet = viewModel::showLinkModifyBottomSheet, + showLinkDetailBottomSheet = viewModel::showLinkDetailBottomSheet, + loadNextLinks = viewModel::loadNextLinks ) } @@ -55,6 +133,7 @@ fun SearchScreen( state: SearchScreenState = SearchScreenState(), currentSearchWord: String = "", linkList: List = emptyList(), + linkPagingState: SimplePagingState = SimplePagingState.IDLE, onClickBack: () -> Unit = {}, inputSearchWord: (String) -> Unit = {}, onClickSearch: () -> Unit = {}, @@ -64,12 +143,15 @@ fun SearchScreen( onClickRemoveRecentSearchWord: (String) -> Unit = {}, onClickFilterSelect: () -> Unit = {}, onClickFilterItem: (FilterType) -> Unit = {}, - hideBottomSheet: () -> Unit = {}, - onClickFilterSave: (Filter) -> Unit = {}, toggleSortOrder: () -> Unit = {}, + showLinkModifyBottomSheet: (Link) -> Unit = {}, + showLinkDetailBottomSheet: (Link) -> Unit = {}, + loadNextLinks: () -> Unit = {}, ) { Column( - modifier = Modifier.fillMaxSize() + modifier = Modifier + .fillMaxSize() + .background(PokitTheme.colors.backgroundBase) ) { Toolbar( onClickBack = onClickBack, @@ -112,16 +194,12 @@ fun SearchScreen( .weight(1f), onToggleSort = toggleSortOrder, useRecentOrder = state.sortRecent, - links = linkList + onClickLinkKebab = showLinkModifyBottomSheet, + onClickLink = showLinkDetailBottomSheet, + links = linkList, + linkPagingState = linkPagingState, + loadNextLinks = loadNextLinks ) } - - FilterBottomSheet( - filter = state.filter ?: Filter(), - firstShowType = state.firstBottomSheetFilterType, - show = state.showFilterBottomSheet, - onDismissRequest = hideBottomSheet, - onSaveClilck = onClickFilterSave - ) } } diff --git a/feature/search/src/main/java/pokitmons/pokit/search/SearchViewModel.kt b/feature/search/src/main/java/pokitmons/pokit/search/SearchViewModel.kt index b8c0055a..7bd984fd 100644 --- a/feature/search/src/main/java/pokitmons/pokit/search/SearchViewModel.kt +++ b/feature/search/src/main/java/pokitmons/pokit/search/SearchViewModel.kt @@ -1,32 +1,87 @@ package pokitmons.pokit.search import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +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 pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.usecase.link.SearchLinksUseCase +import pokitmons.pokit.domain.usecase.link.SetBookmarkUseCase +import pokitmons.pokit.domain.usecase.pokit.GetPokitsUseCase +import pokitmons.pokit.domain.usecase.search.AddRecentSearchWordUseCase +import pokitmons.pokit.domain.usecase.search.GetRecentSearchWordsUseCase +import pokitmons.pokit.domain.usecase.search.GetUseRecentSearchWordsUseCase +import pokitmons.pokit.domain.usecase.search.RemoveRecentSearchWordUseCase +import pokitmons.pokit.domain.usecase.search.SetUseRecentSearchWordsUseCase +import pokitmons.pokit.search.model.BottomSheetType import pokitmons.pokit.search.model.Filter import pokitmons.pokit.search.model.FilterType import pokitmons.pokit.search.model.Link +import pokitmons.pokit.search.model.Pokit import pokitmons.pokit.search.model.SearchScreenState import pokitmons.pokit.search.model.SearchScreenStep -import pokitmons.pokit.search.model.sampleLinks +import pokitmons.pokit.search.paging.LinkPaging +import pokitmons.pokit.search.paging.PokitPaging +import pokitmons.pokit.search.paging.SimplePagingState +import javax.inject.Inject + +@HiltViewModel +class SearchViewModel @Inject constructor( + searchLinksUseCase: SearchLinksUseCase, + getPokitsUseCase: GetPokitsUseCase, + getRecentSearchWordsUseCase: GetRecentSearchWordsUseCase, + getUseRecentSearchWordsUseCase: GetUseRecentSearchWordsUseCase, + private val setUseRecentSearchWordsUseCase: SetUseRecentSearchWordsUseCase, + private val addRecentSearchWordUseCase: AddRecentSearchWordUseCase, + private val removeRecentSearchWordUseCase: RemoveRecentSearchWordUseCase, + private val setBookmarkUseCase: SetBookmarkUseCase, +) : ViewModel() { + private val linkPaging = LinkPaging( + searchLinksUseCase = searchLinksUseCase, + filter = Filter(), + perPage = 10, + coroutineScope = viewModelScope, + initPage = 0 + ) + + private val pokitPaging = PokitPaging( + getPokits = getPokitsUseCase, + perPage = 10, + coroutineScope = viewModelScope, + initPage = 0 + ) + + val linkList: StateFlow> = linkPaging.pagingData + val linkPagingState: StateFlow = linkPaging.pagingState + + val pokitList: StateFlow> = pokitPaging.pagingData + val pokitPagingState: StateFlow = pokitPaging.pagingState -class SearchViewModel : ViewModel() { private val _searchWord = MutableStateFlow("") val searchWord = _searchWord.asStateFlow() private val _state = MutableStateFlow(SearchScreenState()) - val state = _state.asStateFlow() + val state = combine( + _state, + getRecentSearchWordsUseCase.getWords(), + getUseRecentSearchWordsUseCase.getUse() + ) { state, searchWords, useRecentSearchWord -> + state.copy(recentSearchWords = searchWords, useRecentSearchWord = useRecentSearchWord) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = SearchScreenState() + ) private var appliedSearchWord = "" - private val _linkList = MutableStateFlow>(emptyList()) - val linkList = _linkList.asStateFlow() - - init { - _linkList.update { sampleLinks } - } - fun inputSearchWord(newSearchWord: String) { _searchWord.update { newSearchWord } val currentState = state.value @@ -45,6 +100,11 @@ class SearchViewModel : ViewModel() { _state.update { state -> state.copy(step = SearchScreenStep.RESULT) } + viewModelScope.launch { + addRecentSearchWordUseCase.addRecentSearchWord(appliedSearchWord) + linkPaging.changeSearchWord(appliedSearchWord) + linkPaging.refresh() + } } } @@ -56,26 +116,30 @@ class SearchViewModel : ViewModel() { _state.update { state -> state.copy(step = SearchScreenStep.RESULT) } + viewModelScope.launch { + addRecentSearchWordUseCase.addRecentSearchWord(appliedSearchWord) + linkPaging.changeSearchWord(appliedSearchWord) + linkPaging.refresh() + } } } fun toggleUseRecentSearchWord() { - _state.update { state -> - state.copy(useRecentSearchWord = !state.useRecentSearchWord) + val currentUseRecentSearchWord = state.value.useRecentSearchWord + viewModelScope.launch { + setUseRecentSearchWordsUseCase.setUse(!currentUseRecentSearchWord) } } fun removeRecentSearchWord(word: String) { - _state.update { state -> - state.copy(recentSearchWords = state.recentSearchWords.filter { name -> name != word }) + viewModelScope.launch { + removeRecentSearchWordUseCase.removeWord(word) } } fun removeAllRecentSearchWord() { - _state.update { state -> - state.copy( - recentSearchWords = emptyList() - ) + viewModelScope.launch { + removeRecentSearchWordUseCase.removeAll() } } @@ -104,6 +168,53 @@ class SearchViewModel : ViewModel() { } } + fun showLinkModifyBottomSheet(link: Link) { + _state.update { state -> + state.copy( + linkBottomSheetType = BottomSheetType.MODIFY, + currentLink = link + ) + } + } + + fun showLinkRemoveBottomSheet(link: Link) { + _state.update { state -> + state.copy( + linkBottomSheetType = BottomSheetType.REMOVE, + showLinkDetailBottomSheet = false, + currentLink = link + ) + } + } + + fun hideLinkModifyBottomSheet() { + _state.update { state -> + state.copy( + linkBottomSheetType = null, + currentLink = null + ) + } + } + + fun showLinkDetailBottomSheet(link: Link) { + _state.update { state -> + state.copy( + currentLink = link, + showLinkDetailBottomSheet = true, + linkBottomSheetType = null + ) + } + } + + fun hideLinkDetailBottomSheet() { + _state.update { state -> + state.copy( + currentLink = null, + showLinkDetailBottomSheet = false + ) + } + } + fun setFilter(filter: Filter) { _state.update { state -> state.copy( @@ -116,7 +227,10 @@ class SearchViewModel : ViewModel() { ) } - // todo refresh 기능 구현 + viewModelScope.launch { + linkPaging.changeFilter(filter) + linkPaging.refresh() + } } fun toggleSortOrder() { @@ -124,6 +238,46 @@ class SearchViewModel : ViewModel() { state.copy(sortRecent = !state.sortRecent) } - // todo refresh 기능 구현 + viewModelScope.launch { + linkPaging.changeRecentSort(state.value.sortRecent) + linkPaging.refresh() + } + } + + fun loadNextLinks() { + viewModelScope.launch { + linkPaging.load() + } + } + + fun loadNextPokits() { + viewModelScope.launch { + pokitPaging.load() + } + } + + fun refreshPokits() { + viewModelScope.launch { + pokitPaging.refresh() + } + } + + fun toggleBookmark() { + val currentLink = state.value.currentLink ?: return + val currentLinkId = currentLink.id.toIntOrNull() ?: return + val applyBookmarked = !currentLink.bookmark + + viewModelScope.launch { + val response = setBookmarkUseCase.setBookMarked(currentLinkId, applyBookmarked) + if (response is PokitResult.Success) { + val bookmarkChangedLink = currentLink.copy(bookmark = applyBookmarked) + _state.update { state -> + state.copy( + currentLink = bookmarkChangedLink + ) + } + linkPaging.modifyItem(bookmarkChangedLink) + } + } } } diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/filter/Filter.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/filter/Filter.kt index 4f18074a..9db50506 100644 --- a/feature/search/src/main/java/pokitmons/pokit/search/components/filter/Filter.kt +++ b/feature/search/src/main/java/pokitmons/pokit/search/components/filter/Filter.kt @@ -57,41 +57,41 @@ fun FilterArea( ) PokitButton( - text = stringResource(id = R.string.pokit_name), + text = stringResource(id = R.string.collect_show), icon = PokitButtonIcon( resourceId = coreDrawable.icon_24_arrow_down, position = PokitButtonIconPosition.RIGHT ), - onClick = onClickPokitName, + onClick = onClickBookmark, size = PokitButtonSize.SMALL, shape = PokitButtonShape.ROUND, - style = PokitButtonStyle.STROKE, + style = PokitButtonStyle.DEFAULT, type = PokitButtonType.SECONDARY ) PokitButton( - text = stringResource(id = R.string.collect_show), + text = stringResource(id = R.string.period), icon = PokitButtonIcon( resourceId = coreDrawable.icon_24_arrow_down, position = PokitButtonIconPosition.RIGHT ), - onClick = onClickBookmark, + onClick = onClickPeriod, size = PokitButtonSize.SMALL, shape = PokitButtonShape.ROUND, - style = PokitButtonStyle.STROKE, + style = PokitButtonStyle.DEFAULT, type = PokitButtonType.SECONDARY ) PokitButton( - text = stringResource(id = R.string.period), + text = stringResource(id = R.string.pokit), icon = PokitButtonIcon( resourceId = coreDrawable.icon_24_arrow_down, position = PokitButtonIconPosition.RIGHT ), - onClick = onClickPeriod, + onClick = onClickPokitName, size = PokitButtonSize.SMALL, shape = PokitButtonShape.ROUND, - style = PokitButtonStyle.STROKE, + style = PokitButtonStyle.DEFAULT, type = PokitButtonType.SECONDARY ) } else { diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheet.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheet.kt index 9ceff05a..2d0540a3 100644 --- a/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheet.kt +++ b/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheet.kt @@ -1,11 +1,13 @@ package pokitmons.pokit.search.components.filterbottomsheet import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import pokitmons.pokit.core.ui.components.template.bottomsheet.PokitBottomSheet import pokitmons.pokit.search.model.Filter import pokitmons.pokit.search.model.FilterType import pokitmons.pokit.search.model.Pokit import pokitmons.pokit.search.model.samplePokits +import pokitmons.pokit.search.paging.SimplePagingState @Composable fun FilterBottomSheet( @@ -13,6 +15,9 @@ fun FilterBottomSheet( firstShowType: FilterType = FilterType.Pokit, onSaveClilck: (Filter) -> Unit = {}, pokits: List = samplePokits, + pokitPagingState: SimplePagingState = SimplePagingState.IDLE, + loadNextPokits: () -> Unit = {}, + refreshPokits: () -> Unit = {}, show: Boolean = false, onDismissRequest: () -> Unit = {}, ) { @@ -20,11 +25,17 @@ fun FilterBottomSheet( onHideBottomSheet = onDismissRequest, show = show ) { + LaunchedEffect(Unit) { + refreshPokits() + } + FilterBottomSheetContent( filter = filter, firstShowType = firstShowType, onSaveClilck = onSaveClilck, - pokits = pokits + pokits = pokits, + pokitPagingState = pokitPagingState, + loadNextPokits = loadNextPokits ) } } diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheetContent.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheetContent.kt index 63584cd0..64e927d0 100644 --- a/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheetContent.kt +++ b/feature/search/src/main/java/pokitmons/pokit/search/components/filterbottomsheet/FilterBottomSheetContent.kt @@ -16,12 +16,15 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -45,6 +48,7 @@ import pokitmons.pokit.search.model.Filter import pokitmons.pokit.search.model.FilterType import pokitmons.pokit.search.model.Pokit import pokitmons.pokit.search.model.samplePokits +import pokitmons.pokit.search.paging.SimplePagingState import pokitmons.pokit.core.ui.R.string as coreString @OptIn(ExperimentalFoundationApi::class) @@ -54,6 +58,8 @@ fun FilterBottomSheetContent( firstShowType: FilterType = FilterType.Pokit, onSaveClilck: (Filter) -> Unit = {}, pokits: List = samplePokits, + pokitPagingState: SimplePagingState = SimplePagingState.IDLE, + loadNextPokits: () -> Unit = {}, ) { var currentFilter by remember { mutableStateOf(filter) } var currentShowType by remember { mutableStateOf(firstShowType) } @@ -64,6 +70,21 @@ fun FilterBottomSheetContent( FilterType.entries.size } + val linkLazyColumnListState = rememberLazyListState() + val startLinkPaging = remember { + derivedStateOf { + linkLazyColumnListState.layoutInfo.visibleItemsInfo.lastOrNull()?.let { last -> + last.index >= linkLazyColumnListState.layoutInfo.totalItemsCount - 3 + } ?: false + } + } + + LaunchedEffect(startLinkPaging.value) { + if (startLinkPaging.value && pokitPagingState == SimplePagingState.IDLE) { + loadNextPokits() + } + } + Column( modifier = Modifier.fillMaxWidth() ) { @@ -98,7 +119,8 @@ fun FilterBottomSheetContent( when (currentShowType) { FilterType.Pokit -> { LazyColumn( - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), + state = linkLazyColumnListState ) { items(pokits) { pokit -> PokitList( diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/linkdetailbottomsheet/Link.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/linkdetailbottomsheet/Link.kt new file mode 100644 index 00000000..7535141e --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/components/linkdetailbottomsheet/Link.kt @@ -0,0 +1,70 @@ +package pokitmons.pokit.search.components.linkdetailbottomsheet + +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.theme.PokitTheme +import pokitmons.pokit.search.model.Link +import pokitmons.pokit.core.ui.R.drawable as coreDrawable + +@Composable +internal fun Link( + link: Link, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .height(IntrinsicSize.Min) + .border( + width = 1.dp, + color = PokitTheme.colors.borderTertiary, + shape = RoundedCornerShape(12.dp) + ) + ) { + Image( + painter = painterResource(id = coreDrawable.icon_24_google), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.width(124.dp) + ) + + Column( + modifier = Modifier + .padding(start = 16.dp, end = 20.dp, top = 16.dp, bottom = 16.dp) + .weight(1f) + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = link.title, + maxLines = 2, + style = PokitTheme.typography.body3Medium.copy(color = PokitTheme.colors.textSecondary) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + modifier = Modifier.fillMaxWidth(), + text = link.url, + maxLines = 2, + style = PokitTheme.typography.detail2.copy(color = PokitTheme.colors.textTertiary) + ) + } + } +} diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/linkdetailbottomsheet/LinkDetailBottomSheet.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/linkdetailbottomsheet/LinkDetailBottomSheet.kt new file mode 100644 index 00000000..650065e0 --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/components/linkdetailbottomsheet/LinkDetailBottomSheet.kt @@ -0,0 +1,217 @@ +package pokitmons.pokit.search.components.linkdetailbottomsheet + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +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.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.R +import pokitmons.pokit.core.ui.components.template.bottomsheet.PokitBottomSheet +import pokitmons.pokit.core.ui.theme.PokitTheme +import pokitmons.pokit.core.ui.theme.color.Orange50 +import pokitmons.pokit.search.model.Link + +@Composable +fun LinkDetailBottomSheet( + link: Link, + onHideBottomSheet: () -> Unit, + show: Boolean = false, + onClickRemoveLink: (Link) -> Unit, + onClickModifyLink: (Link) -> Unit, + onClickBookmark: () -> Unit, +) { + PokitBottomSheet( + onHideBottomSheet = onHideBottomSheet, + show = show + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.icon_24_bell), + contentDescription = null, + modifier = Modifier + .size(20.dp) + .background( + color = PokitTheme.colors.brand, + shape = CircleShape + ) + .padding(2.dp), + colorFilter = ColorFilter.tint(PokitTheme.colors.inverseWh) + ) + + Spacer(modifier = Modifier.width(4.dp)) + + Text( + text = stringResource(id = link.linkType.textResourceId), + modifier = Modifier + .border( + width = 1.dp, + color = PokitTheme.colors.borderTertiary, + shape = RoundedCornerShape(4.dp) + ) + .background( + color = PokitTheme.colors.backgroundBase, + shape = RoundedCornerShape(4.dp) + ) + .padding(horizontal = 8.dp, vertical = 4.dp), + style = PokitTheme.typography.label4.copy(color = PokitTheme.colors.textTertiary) + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = link.title, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = PokitTheme.typography.title3.copy(color = PokitTheme.colors.textPrimary) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + modifier = Modifier.fillMaxWidth(), + text = link.dateString, + style = PokitTheme.typography.detail2.copy(color = PokitTheme.colors.textTertiary), + textAlign = TextAlign.End + ) + } + + Spacer(modifier = Modifier.height(12.dp)) + + HorizontalDivider( + thickness = 1.dp, + color = PokitTheme.colors.borderTertiary + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 24.dp) + ) { + Link(link = link) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = link.memo, + modifier = Modifier + .fillMaxWidth() + .background( + color = Orange50, + shape = RoundedCornerShape(8.dp) + ) + .padding(16.dp), + style = PokitTheme.typography.body3Regular.copy(color = PokitTheme.colors.textPrimary), + maxLines = 4, + minLines = 4 + ) + } + + HorizontalDivider( + thickness = 1.dp, + color = PokitTheme.colors.borderTertiary + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 10.dp, start = 10.dp, end = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + modifier = Modifier + .size(36.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = onClickBookmark + ) + .padding(6.dp), + painter = painterResource(id = R.drawable.icon_24_star), + contentDescription = "bookmark", + colorFilter = ColorFilter.tint( + color = if (link.bookmark) PokitTheme.colors.brand else PokitTheme.colors.iconTertiary + ) + ) + + Spacer(modifier = Modifier.weight(1f)) + + Image( + modifier = Modifier + .size(36.dp) + .padding(6.dp), + painter = painterResource(id = R.drawable.icon_24_share), + contentDescription = "share", + colorFilter = ColorFilter.tint( + color = PokitTheme.colors.iconSecondary + ) + ) + + Image( + modifier = Modifier + .size(36.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = { + onClickModifyLink(link) + } + ) + .padding(6.dp), + painter = painterResource(id = R.drawable.icon_24_edit), + contentDescription = "edit", + colorFilter = ColorFilter.tint( + color = PokitTheme.colors.iconSecondary + ) + ) + + Image( + modifier = Modifier + .size(36.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = { + onClickRemoveLink(link) + } + ) + .padding(6.dp), + painter = painterResource(id = R.drawable.icon_24_trash), + contentDescription = "remove", + colorFilter = ColorFilter.tint( + color = PokitTheme.colors.iconSecondary + ) + ) + } + } +} diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/searchitemlist/SearchItemList.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/searchitemlist/SearchItemList.kt index c462b3f3..ec696e7f 100644 --- a/feature/search/src/main/java/pokitmons/pokit/search/components/searchitemlist/SearchItemList.kt +++ b/feature/search/src/main/java/pokitmons/pokit/search/components/searchitemlist/SearchItemList.kt @@ -9,8 +9,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -21,6 +24,7 @@ import androidx.compose.ui.unit.dp import pokitmons.pokit.core.ui.components.block.linkcard.LinkCard import pokitmons.pokit.core.ui.theme.PokitTheme import pokitmons.pokit.search.model.Link +import pokitmons.pokit.search.paging.SimplePagingState import pokitmons.pokit.core.ui.R.drawable as coreDrawable import pokitmons.pokit.search.R.string as SearchString @@ -30,9 +34,26 @@ internal fun SearchItemList( onToggleSort: () -> Unit = {}, useRecentOrder: Boolean = true, links: List = emptyList(), + linkPagingState: SimplePagingState = SimplePagingState.IDLE, onClickLinkKebab: (Link) -> Unit = {}, onClickLink: (Link) -> Unit = {}, + loadNextLinks: () -> Unit = {}, ) { + val linkLazyColumnListState = rememberLazyListState() + val startLinkPaging = remember { + derivedStateOf { + linkLazyColumnListState.layoutInfo.visibleItemsInfo.lastOrNull()?.let { last -> + last.index >= linkLazyColumnListState.layoutInfo.totalItemsCount - 3 + } ?: false + } + } + + LaunchedEffect(startLinkPaging.value) { + if (startLinkPaging.value && linkPagingState == SimplePagingState.IDLE) { + loadNextLinks() + } + } + Column( modifier = modifier ) { @@ -60,7 +81,9 @@ internal fun SearchItemList( ) } - LazyColumn { + LazyColumn( + state = linkLazyColumnListState + ) { items(links) { link -> LinkCard( item = link, diff --git a/feature/search/src/main/java/pokitmons/pokit/search/model/Date.kt b/feature/search/src/main/java/pokitmons/pokit/search/model/Date.kt index aa65a803..9e536132 100644 --- a/feature/search/src/main/java/pokitmons/pokit/search/model/Date.kt +++ b/feature/search/src/main/java/pokitmons/pokit/search/model/Date.kt @@ -12,4 +12,8 @@ data class Date( override fun toString(): String { return "${year % 1000}.$month.$day" } + + fun toDateString(): String { + return "%d-%02d-%02d".format(year, month, day) + } } diff --git a/feature/search/src/main/java/pokitmons/pokit/search/model/Link.kt b/feature/search/src/main/java/pokitmons/pokit/search/model/Link.kt index 15121d73..3379f8aa 100644 --- a/feature/search/src/main/java/pokitmons/pokit/search/model/Link.kt +++ b/feature/search/src/main/java/pokitmons/pokit/search/model/Link.kt @@ -1,83 +1,38 @@ package pokitmons.pokit.search.model import pokitmons.pokit.search.R +import pokitmons.pokit.domain.model.link.Link as DomainLink data class Link( - val id: String, - val title: String, - val dateString: String, - val domainUrl: String, - val isRead: Boolean, - val linkType: LinkType, - val url: String, - val memo: String, - val bookmark: Boolean, + val id: String = "", + val title: String = "", + val dateString: String = "", + val domainUrl: String = "", + val isRead: Boolean = false, + val linkType: LinkType = LinkType.TEXT, + val url: String = "", + val memo: String = "", + val bookmark: Boolean = false, val imageUrl: String? = null, -) +) { + companion object { + fun fromDomainLink(domainLink: DomainLink): Link { + return Link( + id = domainLink.id.toString(), + title = domainLink.title, + dateString = domainLink.createdAt, + domainUrl = domainLink.domain, + isRead = domainLink.isRead, + url = domainLink.data, + memo = domainLink.memo, + imageUrl = domainLink.thumbnail, + bookmark = domainLink.favorites, + linkType = LinkType.TEXT + ) + } + } +} enum class LinkType(val textResourceId: Int) { TEXT(R.string.text), } - -internal val sampleLinks = listOf( - Link( - id = "1", - title = "자연 친화적인 라이프스타일을 위한 환경 보호 방법", - imageUrl = null, - dateString = "2024.04.12", - domainUrl = "youtu.be", - isRead = true, - linkType = LinkType.TEXT, - url = "", - memo = "", - bookmark = true - ), - Link( - id = "2", - title = "자연 친화적인 라이프스타일을 위한 환경 보호 방법", - imageUrl = null, - dateString = "2024.05.12", - domainUrl = "youtu.be", - isRead = false, - linkType = LinkType.TEXT, - url = "", - memo = "", - bookmark = true - ), - Link( - id = "3", - title = "포킷포킷", - imageUrl = null, - dateString = "2024.04.12", - domainUrl = "pokitmons.pokit", - isRead = true, - linkType = LinkType.TEXT, - url = "", - memo = "", - bookmark = true - ), - Link( - id = "4", - title = "자연 친화적인 라이프스타일을 위한 환경 보호 방법", - imageUrl = null, - dateString = "2024.06.12", - domainUrl = "youtu.be", - isRead = true, - linkType = LinkType.TEXT, - url = "", - memo = "", - bookmark = true - ), - Link( - id = "5", - title = "마지막 링크입니다.", - imageUrl = null, - dateString = "2024.07.14", - domainUrl = "youtu.be", - isRead = false, - linkType = LinkType.TEXT, - url = "", - memo = "", - bookmark = true - ) -) diff --git a/feature/search/src/main/java/pokitmons/pokit/search/model/Pokit.kt b/feature/search/src/main/java/pokitmons/pokit/search/model/Pokit.kt index bcc29728..99072c5c 100644 --- a/feature/search/src/main/java/pokitmons/pokit/search/model/Pokit.kt +++ b/feature/search/src/main/java/pokitmons/pokit/search/model/Pokit.kt @@ -4,7 +4,17 @@ data class Pokit( val title: String = "", val id: String = "", val count: Int = 0, -) +) { + companion object { + fun fromDomainPokit(pokit: pokitmons.pokit.domain.model.pokit.Pokit): Pokit { + return Pokit( + title = pokit.name, + id = pokit.categoryId.toString(), + count = pokit.linkCount + ) + } + } +} internal val samplePokits = listOf( Pokit(title = "안드로이드", id = "1", count = 2), diff --git a/feature/search/src/main/java/pokitmons/pokit/search/model/SearchScreenState.kt b/feature/search/src/main/java/pokitmons/pokit/search/model/SearchScreenState.kt index 034bf214..b7c33ebe 100644 --- a/feature/search/src/main/java/pokitmons/pokit/search/model/SearchScreenState.kt +++ b/feature/search/src/main/java/pokitmons/pokit/search/model/SearchScreenState.kt @@ -7,9 +7,16 @@ data class SearchScreenState( val useRecentSearchWord: Boolean = false, val showFilterBottomSheet: Boolean = false, val firstBottomSheetFilterType: FilterType = FilterType.Pokit, + val showLinkDetailBottomSheet: Boolean = false, + val linkBottomSheetType: BottomSheetType? = null, val sortRecent: Boolean = true, + val currentLink: Link? = null, ) enum class SearchScreenStep { INPUT, RESULT } + +enum class BottomSheetType { + MODIFY, REMOVE +} diff --git a/feature/search/src/main/java/pokitmons/pokit/search/paging/LinkPaging.kt b/feature/search/src/main/java/pokitmons/pokit/search/paging/LinkPaging.kt new file mode 100644 index 00000000..7646fa85 --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/paging/LinkPaging.kt @@ -0,0 +1,160 @@ +package pokitmons.pokit.search.paging + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.usecase.link.SearchLinksUseCase +import pokitmons.pokit.search.model.Filter +import pokitmons.pokit.search.model.Link +import kotlin.coroutines.cancellation.CancellationException + +class LinkPaging( + private val searchLinksUseCase: SearchLinksUseCase, + private val perPage: Int = 10, + private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO), + private val initPage: Int = 0, + private val firstRequestPage: Int = 3, + private var filter: Filter, + private var searchWord: String = "", + private var recentSort: Boolean = true, +) : SimplePaging { + + private val _pagingState: MutableStateFlow = MutableStateFlow(SimplePagingState.IDLE) + override val pagingState: StateFlow = _pagingState.asStateFlow() + + private val _pagingData: MutableStateFlow> = MutableStateFlow(emptyList()) + override val pagingData: StateFlow> = _pagingData.asStateFlow() + + private var currentPageIndex = initPage + private var requestJob: Job? = null + + fun changeFilter(filter: Filter) { + this.filter = filter + } + + fun changeSearchWord(searchWord: String) { + this.searchWord = searchWord + } + + fun changeRecentSort(recentSort: Boolean) { + this.recentSort = recentSort + } + + override suspend fun refresh() { + requestJob?.cancel() + + _pagingData.update { emptyList() } + _pagingState.update { SimplePagingState.LOADING_INIT } + requestJob = coroutineScope.launch { + try { + currentPageIndex = initPage + val response = searchLinksUseCase.searchLinks( + page = currentPageIndex, + size = perPage * firstRequestPage, + sort = listOf(), + isRead = !filter.notRead, + favorites = filter.bookmark, + startDate = filter.startDate?.toDateString(), + endDate = filter.endDate?.toDateString(), + categoryIds = filter.selectedPokits.mapNotNull { it.id.toIntOrNull() }, + searchWord = searchWord + ) + when (response) { + is PokitResult.Success -> { + val links = response.result.map { domainLink -> + Link.fromDomainLink(domainLink) + } + applyResponse(links, firstRequestPage) + } + is PokitResult.Error -> { + _pagingState.update { SimplePagingState.FAILURE_INIT } + } + } + } catch (exception: Exception) { + if (exception !is CancellationException) { + _pagingState.update { SimplePagingState.FAILURE_INIT } + } + } + } + } + + override suspend fun load() { + if (pagingState.value != SimplePagingState.IDLE) return + + requestJob?.cancel() + _pagingState.update { SimplePagingState.LOADING_NEXT } + + requestJob = coroutineScope.launch { + try { + val response = searchLinksUseCase.searchLinks( + page = currentPageIndex, + size = perPage, + sort = listOf(), + isRead = !filter.notRead, + favorites = filter.bookmark, + startDate = filter.startDate?.toDateString(), + endDate = filter.endDate?.toDateString(), + categoryIds = filter.selectedPokits.mapNotNull { it.id.toIntOrNull() }, + searchWord = searchWord + ) + when (response) { + is PokitResult.Success -> { + val links = response.result.map { domainLink -> + Link.fromDomainLink(domainLink) + } + applyResponse(links) + } + is PokitResult.Error -> { + _pagingState.update { SimplePagingState.FAILURE_INIT } + } + } + } catch (exception: Exception) { + if (exception !is CancellationException) { + _pagingState.update { SimplePagingState.FAILURE_NEXT } + } + } + } + } + + private fun applyResponse(dataInResponse: List, multiple: Int = 1) { + if (dataInResponse.size < perPage * multiple) { + _pagingState.update { SimplePagingState.LAST } + } else { + _pagingState.update { SimplePagingState.IDLE } + } + _pagingData.update { _pagingData.value + dataInResponse } + currentPageIndex += multiple + } + + override fun clear() { + requestJob?.cancel() + _pagingData.update { emptyList() } + _pagingState.update { SimplePagingState.IDLE } + } + + override suspend fun deleteItem(item: Link) { + val capturedDataList = _pagingData.value + _pagingData.update { capturedDataList.filter { it.id != item.id } } + } + + override suspend fun modifyItem(item: Link) { + val capturedDataList = _pagingData.value + val targetPokit = capturedDataList.find { it.id == item.id } ?: return + + _pagingData.update { + capturedDataList.map { pokit -> + if (targetPokit.id == pokit.id) { + item + } else { + pokit + } + } + } + } +} diff --git a/feature/search/src/main/java/pokitmons/pokit/search/paging/PokitPaging.kt b/feature/search/src/main/java/pokitmons/pokit/search/paging/PokitPaging.kt new file mode 100644 index 00000000..b3a6ae54 --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/paging/PokitPaging.kt @@ -0,0 +1,124 @@ +package pokitmons.pokit.search.paging + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.usecase.pokit.GetPokitsUseCase +import pokitmons.pokit.search.model.Pokit +import kotlin.coroutines.cancellation.CancellationException + +class PokitPaging( + private val getPokits: GetPokitsUseCase, + private val perPage: Int = 10, + private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO), + private val initPage: Int = 0, + private val firstRequestPage: Int = 3, +) : SimplePaging { + private val _pagingState = MutableStateFlow(SimplePagingState.IDLE) + override val pagingState: StateFlow = _pagingState.asStateFlow() + + private val _pagingData: MutableStateFlow> = MutableStateFlow(emptyList()) + override val pagingData: StateFlow> = _pagingData.asStateFlow() + private var currentPageIndex = initPage + private var requestJob: Job? = null + + override suspend fun refresh() { + requestJob?.cancel() + + _pagingData.update { emptyList() } + _pagingState.update { SimplePagingState.LOADING_INIT } + requestJob = coroutineScope.launch { + try { + currentPageIndex = initPage + val response = getPokits.getPokits(size = perPage * firstRequestPage, page = currentPageIndex) + when (response) { + is PokitResult.Success -> { + val pokitList = response.result.map { domainPokit -> + Pokit.fromDomainPokit(domainPokit) + } + applyResponse(pokitList, firstRequestPage) + } + + is PokitResult.Error -> { + _pagingState.update { SimplePagingState.FAILURE_INIT } + } + } + } catch (exception: Exception) { + if (exception !is CancellationException) { + _pagingState.update { SimplePagingState.FAILURE_INIT } + } + } + } + } + + override suspend fun load() { + if (pagingState.value != SimplePagingState.IDLE) return + + requestJob?.cancel() + _pagingState.update { SimplePagingState.LOADING_NEXT } + + requestJob = coroutineScope.launch { + try { + val response = getPokits.getPokits(size = perPage, page = currentPageIndex) + when (response) { + is PokitResult.Success -> { + val pokitList = response.result.map { domainPokit -> + Pokit.fromDomainPokit(domainPokit) + } + applyResponse(pokitList) + } + + is PokitResult.Error -> { + _pagingState.update { SimplePagingState.FAILURE_NEXT } + } + } + } catch (exception: Exception) { + if (exception !is CancellationException) { + _pagingState.update { SimplePagingState.FAILURE_NEXT } + } + } + } + } + + private fun applyResponse(dataInResponse: List, multiple: Int = 1) { + if (dataInResponse.size < perPage * multiple) { + _pagingState.update { SimplePagingState.LAST } + } else { + _pagingState.update { SimplePagingState.IDLE } + } + _pagingData.update { _pagingData.value + dataInResponse } + currentPageIndex += multiple + } + + override fun clear() { + requestJob?.cancel() + _pagingData.update { emptyList() } + _pagingState.update { SimplePagingState.IDLE } + } + + override suspend fun deleteItem(item: Pokit) { + val capturedDataList = _pagingData.value + _pagingData.update { capturedDataList.filter { it.id != item.id } } + } + + override suspend fun modifyItem(item: Pokit) { + val capturedDataList = _pagingData.value + val targetPokit = capturedDataList.find { it.id == item.id } ?: return + + _pagingData.update { + capturedDataList.map { pokit -> + if (targetPokit.id == pokit.id) { + item + } else { + pokit + } + } + } + } +} diff --git a/feature/search/src/main/java/pokitmons/pokit/search/paging/SimplePaging.kt b/feature/search/src/main/java/pokitmons/pokit/search/paging/SimplePaging.kt new file mode 100644 index 00000000..b80d630c --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/paging/SimplePaging.kt @@ -0,0 +1,13 @@ +package pokitmons.pokit.search.paging + +import kotlinx.coroutines.flow.Flow + +interface SimplePaging { + val pagingData: Flow> + suspend fun refresh() + suspend fun load() + val pagingState: Flow + suspend fun modifyItem(item: T) + suspend fun deleteItem(item: T) + fun clear() +} diff --git a/feature/search/src/main/java/pokitmons/pokit/search/paging/SimplePagingState.kt b/feature/search/src/main/java/pokitmons/pokit/search/paging/SimplePagingState.kt new file mode 100644 index 00000000..50a931f1 --- /dev/null +++ b/feature/search/src/main/java/pokitmons/pokit/search/paging/SimplePagingState.kt @@ -0,0 +1,5 @@ +package pokitmons.pokit.search.paging + +enum class SimplePagingState { + IDLE, LOADING_NEXT, LOADING_INIT, FAILURE_NEXT, FAILURE_INIT, LAST +} diff --git a/feature/search/src/main/res/values/string.xml b/feature/search/src/main/res/values/string.xml index baceab83..90377eb6 100644 --- a/feature/search/src/main/res/values/string.xml +++ b/feature/search/src/main/res/values/string.xml @@ -32,4 +32,7 @@ %d년 %d월 + + 링크를 정말 삭제하시겠습니까? + 함께 저장한 모든 정보가 삭제되며,\n복구하실 수 없습니다. \ No newline at end of file diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts new file mode 100644 index 00000000..ff2bb5e9 --- /dev/null +++ b/feature/settings/build.gradle.kts @@ -0,0 +1,74 @@ +plugins { + alias(libs.plugins.com.android.library) + alias(libs.plugins.org.jetbrains.kotlin.android) + alias(libs.plugins.hilt) + id("kotlin-kapt") +} + +android { + namespace = "pokitmons.pokit.settings" + compileSdk = 34 + + defaultConfig { + minSdk = 24 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.5.1" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + implementation(libs.androidx.navigation.compose) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) + + implementation(project(":core:ui")) + implementation(project(":domain")) + implementation(project(":feature:login")) + + // hilt + implementation(libs.hilt) + kapt(libs.hilt.compiler) + + // navigation + implementation(libs.androidx.navigation.compose) + implementation(libs.hilt.navigation.compose) +} diff --git a/feature/settings/proguard-rules.pro b/feature/settings/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/settings/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/settings/src/androidTest/java/pokitmons/pokit/settings/ExampleInstrumentedTest.kt b/feature/settings/src/androidTest/java/pokitmons/pokit/settings/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..69e8d671 --- /dev/null +++ b/feature/settings/src/androidTest/java/pokitmons/pokit/settings/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package pokitmons.pokit.settings + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import junit.framework.TestCase.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("pokitmons.pokit.settings.test", appContext.packageName) + } +} diff --git a/feature/settings/src/main/AndroidManifest.xml b/feature/settings/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/settings/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/settings/src/main/java/pokitmons/pokit/settings/SettingViewModel.kt b/feature/settings/src/main/java/pokitmons/pokit/settings/SettingViewModel.kt new file mode 100644 index 00000000..1c07a18b --- /dev/null +++ b/feature/settings/src/main/java/pokitmons/pokit/settings/SettingViewModel.kt @@ -0,0 +1,80 @@ +package pokitmons.pokit.settings + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.usecase.auth.InputNicknameUseCase +import pokitmons.pokit.domain.usecase.setting.EditNicknameUseCase +import pokitmons.pokit.model.DuplicateNicknameState +import javax.inject.Inject + +@HiltViewModel +class SettingViewModel @Inject constructor( + private val nicknameUseCase: InputNicknameUseCase, + private val editNicknameUseCase: EditNicknameUseCase, +) : ViewModel() { + private var duplicateNicknameJob: Job? = null + + private val _isBottomSheetVisible: MutableState = mutableStateOf(false) + val isBottomSheetVisible: Boolean + get() = _isBottomSheetVisible.value + + private val _inputNicknameState = MutableStateFlow(DuplicateNicknameState()) + val inputNicknameState: StateFlow + get() = _inputNicknameState.asStateFlow() + + fun changeBottomSheetHideState() { + _isBottomSheetVisible.value = !_isBottomSheetVisible.value + } + + fun inputText(inputNickname: String) { + _inputNicknameState.update { duplicateNicknameState -> + duplicateNicknameState.copy(nickname = inputNickname) + } + } + + fun checkDuplicateNickname(nickname: String) { + duplicateNicknameJob?.cancel() + duplicateNicknameJob = viewModelScope.launch { + delay(1.second()) + when (val duplicateNicknameResult = nicknameUseCase.checkDuplicateNickname(nickname)) { + is PokitResult.Success -> { + _inputNicknameState.update { duplicateNicknameState -> + duplicateNicknameState.copy(isDuplicate = duplicateNicknameResult.result.isDuplicate) + } + } + + is PokitResult.Error -> {} + } + } + } + + fun editNickname() { + viewModelScope.launch { + when (val editNicknameResult = editNicknameUseCase.editNickname(_inputNicknameState.value.nickname)) { + is PokitResult.Success -> { + } + + is PokitResult.Error -> { + } + } + } + } + + // TODO 확장함수 모듈 생성하기 + companion object { + private fun Int.second(): Long { + return (this * 1000L) + } + } +} diff --git a/feature/settings/src/main/java/pokitmons/pokit/settings/nickname/EditNicknameScreen.kt b/feature/settings/src/main/java/pokitmons/pokit/settings/nickname/EditNicknameScreen.kt new file mode 100644 index 00000000..1347258b --- /dev/null +++ b/feature/settings/src/main/java/pokitmons/pokit/settings/nickname/EditNicknameScreen.kt @@ -0,0 +1,74 @@ +package pokitmons.pokit.settings.nickname + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.components.atom.button.PokitButton +import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonSize +import pokitmons.pokit.core.ui.components.block.labeledinput.LabeledInput +import pokitmons.pokit.settings.SettingViewModel +import pokitmons.pokit.settings.R.string as StringResource + +private const val NICKNAME_MAX_LENGTH = 10 // TODO 매직넘버를 포함하는 모듈화 추가 후 마이그레이션 예정 + +@Composable +fun EditNicknameScreen( + settingViewModel: SettingViewModel, + onBackPressed: () -> Unit, +) { + val inputNicknameState by settingViewModel.inputNicknameState.collectAsState() + + Box( + modifier = Modifier + .padding(start = 20.dp, end = 20.dp, top = 20.dp, bottom = 28.dp) + .fillMaxSize() + ) { + Column { + NicknameHeader(onBackPressed) + LabeledInput( + modifier = Modifier + .fillMaxWidth() + .imePadding(), + label = "", + inputText = inputNicknameState.nickname, + maxLength = NICKNAME_MAX_LENGTH, + sub = when { + inputNicknameState.nickname.length < NICKNAME_MAX_LENGTH -> stringResource(id = StringResource.input_restriction_message) + !inputNicknameState.isDuplicate -> stringResource(id = StringResource.nickname_already_in_use) + else -> stringResource(id = StringResource.input_max_length) + }, + isError = inputNicknameState.nickname.length > NICKNAME_MAX_LENGTH || !inputNicknameState.isDuplicate, + hintText = stringResource(id = StringResource.input_nickname_hint), + onChangeText = { text -> + if (text.length <= NICKNAME_MAX_LENGTH) { + settingViewModel.apply { + inputText(text) + checkDuplicateNickname(text) + } + } + } + ) + } + + PokitButton( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter), + text = stringResource(id = StringResource.save), + icon = null, + size = PokitButtonSize.LARGE, + enable = !inputNicknameState.isDuplicate, + onClick = { settingViewModel.editNickname() } + ) + } +} diff --git a/feature/settings/src/main/java/pokitmons/pokit/settings/nickname/NicknameHeader.kt b/feature/settings/src/main/java/pokitmons/pokit/settings/nickname/NicknameHeader.kt new file mode 100644 index 00000000..7e21063d --- /dev/null +++ b/feature/settings/src/main/java/pokitmons/pokit/settings/nickname/NicknameHeader.kt @@ -0,0 +1,42 @@ +package pokitmons.pokit.settings.nickname + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.theme.PokitTheme +import pokitmons.pokit.core.ui.R.drawable as DrawableResource +import pokitmons.pokit.settings.R.string as StringResource + +@Composable +fun NicknameHeader(onBackPressed: () -> Unit) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + ) { + Icon( + painter = painterResource(id = DrawableResource.icon_24_arrow_left), + contentDescription = null, + modifier = Modifier + .align(Alignment.CenterStart) + .clickable { onBackPressed() } + .size(24.dp) + ) + + Text( + text = stringResource(StringResource.edit_nickname_title), + style = PokitTheme.typography.title3, + modifier = Modifier.align(Alignment.Center) + ) + } +} diff --git a/feature/settings/src/main/java/pokitmons/pokit/settings/setting/DividerItem.kt b/feature/settings/src/main/java/pokitmons/pokit/settings/setting/DividerItem.kt new file mode 100644 index 00000000..f1e96d3d --- /dev/null +++ b/feature/settings/src/main/java/pokitmons/pokit/settings/setting/DividerItem.kt @@ -0,0 +1,25 @@ +package pokitmons.pokit.settings.setting + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.theme.PokitTheme + +@Composable +fun DividerItem() { + Column(modifier = Modifier.fillMaxWidth()) { + Spacer(modifier = Modifier.height(16.dp)) + Spacer( + modifier = Modifier + .height(6.dp) + .fillMaxWidth() + .background(color = PokitTheme.colors.backgroundPrimary) + ) + Spacer(modifier = Modifier.height(16.dp)) + } +} diff --git a/feature/settings/src/main/java/pokitmons/pokit/settings/setting/SettingHeader.kt b/feature/settings/src/main/java/pokitmons/pokit/settings/setting/SettingHeader.kt new file mode 100644 index 00000000..aeaa575b --- /dev/null +++ b/feature/settings/src/main/java/pokitmons/pokit/settings/setting/SettingHeader.kt @@ -0,0 +1,44 @@ +package pokitmons.pokit.settings.setting + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +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.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.theme.PokitTheme +import pokitmons.pokit.core.ui.R.drawable as DrawableResource +import pokitmons.pokit.settings.R.string as StringResource + +@Composable +fun SettingHeader() { + Box( + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + .padding(start = 24.dp, end = 24.dp) + ) { + Icon( + painter = painterResource(id = DrawableResource.icon_24_arrow_left), + contentDescription = null, + modifier = Modifier + .align(Alignment.CenterStart) + .clickable { } + .size(24.dp) + ) + + Text( + text = stringResource(StringResource.settings), + style = PokitTheme.typography.title3, + modifier = Modifier.align(Alignment.Center) + ) + } +} diff --git a/feature/settings/src/main/java/pokitmons/pokit/settings/setting/SettingItem.kt b/feature/settings/src/main/java/pokitmons/pokit/settings/setting/SettingItem.kt new file mode 100644 index 00000000..151a0aba --- /dev/null +++ b/feature/settings/src/main/java/pokitmons/pokit/settings/setting/SettingItem.kt @@ -0,0 +1,54 @@ +package pokitmons.pokit.settings.setting + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.R +import pokitmons.pokit.core.ui.theme.PokitTheme + +@Composable +fun SettingItem( + title: String, + onClick: () -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + .padding(start = 24.dp, end = 24.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + onClick() + }, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + style = PokitTheme.typography.title3 + ) + + Spacer(modifier = Modifier.weight(1f)) + + Icon( + painter = painterResource(id = R.drawable.icon_24_arrow_right), + contentDescription = null, + modifier = Modifier + .size(24.dp) + ) + } +} diff --git a/feature/settings/src/main/java/pokitmons/pokit/settings/setting/SettingsScreen.kt b/feature/settings/src/main/java/pokitmons/pokit/settings/setting/SettingsScreen.kt new file mode 100644 index 00000000..750dddcb --- /dev/null +++ b/feature/settings/src/main/java/pokitmons/pokit/settings/setting/SettingsScreen.kt @@ -0,0 +1,65 @@ +package pokitmons.pokit.settings.setting + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.components.template.bottomsheet.PokitBottomSheet +import pokitmons.pokit.core.ui.components.template.removeItemBottomSheet.TwoButtonBottomSheetContent +import pokitmons.pokit.settings.SettingViewModel +import pokitmons.pokit.settings.R.string as StringResource + +@Composable +fun SettingsScreen( + settingViewModel: SettingViewModel, + onNavigateToEditNickname: () -> Unit, +) { + Column(modifier = Modifier.fillMaxWidth()) { + SettingHeader() + Spacer(modifier = Modifier.height(16.dp)) + Column(modifier = Modifier.fillMaxWidth()) { + SettingItem(title = stringResource(StringResource.nickname_settings)) { + onNavigateToEditNickname() + } + SettingItem(title = stringResource(StringResource.notification_settings)) { + } + + DividerItem() + + SettingItem(title = stringResource(StringResource.announcements)) { + } + SettingItem(title = stringResource(StringResource.terms_of_service)) { + } + SettingItem(title = stringResource(StringResource.privacy_policy)) { + } + SettingItem(title = stringResource(StringResource.customer_support)) { + } + + DividerItem() + + SettingItem(title = stringResource(StringResource.logout)) { + settingViewModel.changeBottomSheetHideState() + } + SettingItem(title = stringResource(StringResource.delete_account)) { + settingViewModel.changeBottomSheetHideState() + } + } + } + + // TODO 회원탈퇴 분기 + PokitBottomSheet( + onHideBottomSheet = { }, + show = settingViewModel.isBottomSheetVisible + ) { + TwoButtonBottomSheetContent( + title = stringResource(id = StringResource.logout_title), + rightButtonText = stringResource(id = StringResource.logout), + onClickLeftButton = { settingViewModel.changeBottomSheetHideState() }, + onClickRightButton = {} + ) + } +} diff --git a/feature/settings/src/main/res/values/strings.xml b/feature/settings/src/main/res/values/strings.xml new file mode 100644 index 00000000..d6d42fce --- /dev/null +++ b/feature/settings/src/main/res/values/strings.xml @@ -0,0 +1,20 @@ + + 설정 + 닉네임 설정 + 알림 설정 + 공지사항 + 서비스 이용약관 + 개인정보 처리방침 + 고객문의 + 로그아웃 + 회원탈퇴 + 로그아웃하시겠습니까? + 회원 탈퇴하시겠습니까? + 함께 저장된 모든 정보가 삭제되며, 복구하실 수 없습니다. + 닉네임 설정 + 저장 + 한글, 영어, 숫자로만 입력이 가능합니다. + 사용 중인 닉네임입니다. + 최대 20자까지 입력 가능합니다. + 닉네임을 입력해주세요 + \ No newline at end of file diff --git a/feature/settings/src/test/java/pokitmons/pokit/settings/ExampleUnitTest.kt b/feature/settings/src/test/java/pokitmons/pokit/settings/ExampleUnitTest.kt new file mode 100644 index 00000000..df53e25c --- /dev/null +++ b/feature/settings/src/test/java/pokitmons/pokit/settings/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package pokitmons.pokit.settings + +import junit.framework.TestCase.assertEquals +import org.junit.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 51d06d9b..0f90cb4c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,6 +32,7 @@ firebase-auth = "23.0.0" google-services = "4.4.2" datastore-core-android = "1.1.1" datastore-preferences-core-jvm = "1.1.1" +mockk = "1.13.12" [libraries] androidx-lifecycle-service = { group = "androidx.lifecycle", name = "lifecycle-service", version.ref = "lifecycle-runtime-ktx"} @@ -88,6 +89,11 @@ annotation = { group = "androidx.annotation", name = "annotation", version.ref = androidx-datastore-core-android = { group = "androidx.datastore", name = "datastore-core-android", version.ref = "datastore-core-android" } androidx-datastore-preferences-core-jvm = { group = "androidx.datastore", name = "datastore-preferences-core-jvm", version.ref = "datastore-preferences-core-jvm" } +mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } +mockk-android = { group = "io.mockk", name = "mockk-android", version.ref = "mockk" } + +jsoup = { group = "org.jsoup", name = "jsoup", version = "1.18.1" } + [plugins] com-android-application = { id = "com.android.application", version.ref = "agp" } org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "org-jetbrains-kotlin-android" } diff --git a/settings.gradle.kts b/settings.gradle.kts index b862fac0..f0c4e192 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -27,4 +27,9 @@ include(":feature:login") include(":core:ui") include(":feature:addlink") include(":feature:addpokit") +include(":feature:login") include(":feature:pokitdetail") +include(":feature:search") +include(":feature:settings") +include(":feature:alarm") +include(":feature:home")