diff --git a/core/designsystem/src/main/res/raw/dot_loading.json b/core/designsystem/src/main/res/raw/dot_loading.json new file mode 100644 index 00000000..efab1670 --- /dev/null +++ b/core/designsystem/src/main/res/raw/dot_loading.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.5.7","a":"","k":"","d":"","tc":""},"fr":30,"ip":0,"op":46,"w":240,"h":240,"nm":"Comp 2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"dot1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[118.75,118.4,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":6,"s":[118.75,98.4,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":20,"s":[118.75,128.4,0],"to":[0,0,0],"ti":[0,0,0]},{"t":26,"s":[118.75,118.4,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[25,25],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.901960784314,0.905882352941,0.921568627451,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-53.348,2.152],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":46,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"dot2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[173.375,118.4,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":16,"s":[173.375,98.4,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[173.375,128.4,0],"to":[0,0,0],"ti":[0,0,0]},{"t":36,"s":[173.375,118.4,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[25,25],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.901960784314,0.905882352941,0.921568627451,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-53.348,2.152],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":46,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"dot3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":20,"s":[228.75,118.4,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":26,"s":[228.75,98.4,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":40,"s":[228.75,128.4,0],"to":[0,0,0],"ti":[0,0,0]},{"t":46,"s":[228.75,118.4,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[25,25],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.901960784314,0.905882352941,0.921568627451,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-53.348,2.152],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":46,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/data/src/main/kotlin/com/mashup/dorabangs/data/database/PostDao.kt b/data/src/main/kotlin/com/mashup/dorabangs/data/database/PostDao.kt index 7e6ea62d..3b1883e8 100644 --- a/data/src/main/kotlin/com/mashup/dorabangs/data/database/PostDao.kt +++ b/data/src/main/kotlin/com/mashup/dorabangs/data/database/PostDao.kt @@ -15,6 +15,9 @@ interface PostDao { @Query("SELECT * FROM localPostItemEntity") suspend fun getPostByPage(): List + @Query("SELECT * FROM localPostItemEntity ORDER BY createdAt DESC LIMIT :limit") + suspend fun getRecentPosts(limit: Int = 10): List + @Query( """ SELECT * FROM localPostItemEntity @@ -37,6 +40,9 @@ interface PostDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertAll(posts: List) + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertPost(post: LocalPostItemEntity) + /** * Post 아이템 삭제 */ diff --git a/data/src/main/kotlin/com/mashup/dorabangs/data/repository/PostsRepositoryImpl.kt b/data/src/main/kotlin/com/mashup/dorabangs/data/repository/PostsRepositoryImpl.kt index 55388d92..b6467283 100644 --- a/data/src/main/kotlin/com/mashup/dorabangs/data/repository/PostsRepositoryImpl.kt +++ b/data/src/main/kotlin/com/mashup/dorabangs/data/repository/PostsRepositoryImpl.kt @@ -14,11 +14,13 @@ import com.mashup.dorabangs.data.model.toDomain import com.mashup.dorabangs.data.pagingsource.PostRemoteMediator import com.mashup.dorabangs.data.utils.PAGING_SIZE import com.mashup.dorabangs.data.utils.doraPager +import com.mashup.dorabangs.domain.model.AIStatus import com.mashup.dorabangs.domain.model.DoraSampleResponse import com.mashup.dorabangs.domain.model.Link import com.mashup.dorabangs.domain.model.Post import com.mashup.dorabangs.domain.model.PostInfo import com.mashup.dorabangs.domain.model.Posts +import com.mashup.dorabangs.domain.model.PostsMetaData import com.mashup.dorabangs.domain.repository.PostsRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -52,7 +54,14 @@ class PostsRepositoryImpl @Inject constructor( postsRemoteDataSource.saveLink(link) override suspend fun getPost(postId: String) = - postsRemoteDataSource.getPost(postId).toDomain() + postsRemoteDataSource + .getPost(postId) + .toDomain() + .apply { + if (aiStatus == AIStatus.SUCCESS) { + database.postDao().insertPost(toLocalEntity()) + } + } override suspend fun patchPostInfo(postId: String, postInfo: PostInfo): DoraSampleResponse = runCatching { @@ -139,5 +148,15 @@ class PostsRepositoryImpl @Inject constructor( order = order, favorite = favorite, isRead = isRead, - ).toDomain() + ) + .toDomain() + .apply { + val localPostItems = items.map { it.toLocalEntity() } + database.postDao().insertAll(localPostItems) + } + + override suspend fun getLocalPosts(limit: Int) = Posts( + items = database.postDao().getRecentPosts(limit).map { it.toPost() }, + metaData = PostsMetaData(), + ) } diff --git a/domain/src/main/kotlin/com/mashup/dorabangs/domain/repository/PostsRepository.kt b/domain/src/main/kotlin/com/mashup/dorabangs/domain/repository/PostsRepository.kt index 2677f780..defe1c0e 100644 --- a/domain/src/main/kotlin/com/mashup/dorabangs/domain/repository/PostsRepository.kt +++ b/domain/src/main/kotlin/com/mashup/dorabangs/domain/repository/PostsRepository.kt @@ -56,4 +56,6 @@ interface PostsRepository { favorite: Boolean?, isRead: Boolean?, ): Posts + + suspend fun getLocalPosts(limit: Int): Posts } diff --git a/domain/src/main/kotlin/com/mashup/dorabangs/domain/usecase/posts/GetLocalPostsUseCase.kt b/domain/src/main/kotlin/com/mashup/dorabangs/domain/usecase/posts/GetLocalPostsUseCase.kt new file mode 100644 index 00000000..8be5036a --- /dev/null +++ b/domain/src/main/kotlin/com/mashup/dorabangs/domain/usecase/posts/GetLocalPostsUseCase.kt @@ -0,0 +1,11 @@ +package com.mashup.dorabangs.domain.usecase.posts + +import com.mashup.dorabangs.domain.repository.PostsRepository +import javax.inject.Inject + +class GetLocalPostsUseCase @Inject constructor( + private val postsRepository: PostsRepository, +) { + + suspend operator fun invoke(limit: Int) = postsRepository.getLocalPosts(limit) +} diff --git a/feature/home/src/main/java/com/mashup/dorabangs/feature/home/HomeRoute.kt b/feature/home/src/main/java/com/mashup/dorabangs/feature/home/HomeRoute.kt index 5dc701c1..bed883d9 100644 --- a/feature/home/src/main/java/com/mashup/dorabangs/feature/home/HomeRoute.kt +++ b/feature/home/src/main/java/com/mashup/dorabangs/feature/home/HomeRoute.kt @@ -65,6 +65,10 @@ fun HomeRoute( scrollCache = viewModel.scrollCache, ) + LifecycleEventEffect(Lifecycle.Event.ON_CREATE) { + viewModel.loadCachedPosts() + } + LifecycleEventEffect(Lifecycle.Event.ON_START) { if (state.isNeedToRefreshOnStart) { viewModel.setAIClassificationCount() @@ -167,20 +171,18 @@ fun BoxScope.HomeSideEffectUI( state: HomeState, snackBarHostState: SnackbarHostState, toastSnackBarHostState: SnackbarHostState, - navigateToSaveScreenWithLink: (String) -> Unit, - - setLocalCopiedUrl: (String) -> Unit, - getLocalCopiedUrl: suspend () -> String?, showSnackBar: (String) -> Unit, + navigateToSaveScreenWithLink: (String) -> Unit, hideSnackBar: () -> Unit, + getLocalCopiedUrl: suspend () -> String?, + getCustomFolderList: () -> Unit, + setLocalCopiedUrl: (String) -> Unit, setVisibleMoreButtonBottomSheet: (Boolean) -> Unit, setVisibleMovingFolderBottomSheet: (Boolean, Boolean) -> Unit, setVisibleDialog: (Boolean) -> Unit, - getCustomFolderList: () -> Unit, updateSelectFolderId: (String, String) -> Unit, deletePost: (String) -> Unit, moveFolder: (String, String, String) -> Unit, - view: View = LocalView.current, clipboardManager: ClipboardManager = LocalClipboardManager.current, ) { diff --git a/feature/home/src/main/java/com/mashup/dorabangs/feature/home/HomeScreen.kt b/feature/home/src/main/java/com/mashup/dorabangs/feature/home/HomeScreen.kt index 15aad26f..c208e3d0 100644 --- a/feature/home/src/main/java/com/mashup/dorabangs/feature/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/mashup/dorabangs/feature/home/HomeScreen.kt @@ -77,60 +77,62 @@ fun HomeScreen( Box( modifier = modifier.fillMaxSize(), ) { - if (state.isLoading) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(top = 104.dp) - .align(Alignment.Center), - ) { - LottieLoader( - lottieRes = R.raw.spinner, - iterations = Int.MAX_VALUE, + if (state.postList.isEmpty()) { + if (state.isLoading) { + Box( modifier = Modifier - .size(54.dp) + .fillMaxSize() + .padding(top = 104.dp) .align(Alignment.Center), - ) - } - } else if (state.postList.isEmpty()) { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Spacer(modifier = Modifier.height(104.dp)) - - if (state.selectedIndex == 0) { - HomeCarousel( + ) { + LottieLoader( + lottieRes = R.raw.spinner, + iterations = Int.MAX_VALUE, modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp, horizontal = 20.dp) - .clip(DoraRoundTokens.Round20), - homeCarouselItems = listOf( - HomeCarouselItem( - lottieRes = R.raw.unread, - description = stringResource(id = R.string.home_carousel_save_introduce), - onClickButton = navigateToHomeTutorial, - ), - ), + .size(54.dp) + .align(Alignment.Center), ) } - + } else { Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, ) { - Icon( - painter = painterResource(id = R.drawable.ic_empty), - contentDescription = "", - tint = Color.Unspecified, - ) - Text( - modifier = Modifier.padding(top = 12.dp), - text = stringResource(id = R.string.home_empty_feed), - style = DoraTypoTokens.caption3Medium, - color = DoraColorTokens.G3, - ) + Spacer(modifier = Modifier.height(104.dp)) + + if (state.selectedIndex == 0) { + HomeCarousel( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp, horizontal = 20.dp) + .clip(DoraRoundTokens.Round20), + homeCarouselItems = listOf( + HomeCarouselItem( + lottieRes = R.raw.unread, + description = stringResource(id = R.string.home_carousel_save_introduce), + onClickButton = navigateToHomeTutorial, + ), + ), + ) + } + + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize(), + ) { + Icon( + painter = painterResource(id = R.drawable.ic_empty), + contentDescription = "", + tint = Color.Unspecified, + ) + Text( + modifier = Modifier.padding(top = 12.dp), + text = stringResource(id = R.string.home_empty_feed), + style = DoraTypoTokens.caption3Medium, + color = DoraColorTokens.G3, + ) + } } } } else { @@ -226,21 +228,49 @@ fun HomeScreen( ) } - Box( + Column( modifier = Modifier - .padding(bottom = 20.dp, end = 20.dp) - .size(60.dp) - .clip(DoraRoundTokens.Round99) - .background(DoraColorTokens.SurfaceBlack) - .align(Alignment.BottomEnd) - .clickable(onClick = navigateSaveScreenWithoutLink), - contentAlignment = Alignment.Center, + .fillMaxWidth() + .align(Alignment.BottomCenter), ) { - Icon( - tint = DoraColorTokens.G3, - painter = painterResource(id = R.drawable.ic_fab_add), - contentDescription = "", - ) + Box( + modifier = Modifier + .padding(bottom = 20.dp, end = 20.dp) + .size(60.dp) + .clip(DoraRoundTokens.Round99) + .background(DoraColorTokens.SurfaceBlack) + .clickable(onClick = navigateSaveScreenWithoutLink) + .align(Alignment.End), + contentAlignment = Alignment.Center, + ) { + Icon( + tint = DoraColorTokens.G3, + painter = painterResource(id = R.drawable.ic_fab_add), + contentDescription = "", + ) + } + + if (state.isLoading && state.postList.isNotEmpty()) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(36.dp) + .background(DoraColorTokens.SurfaceBlack), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.padding(start = 20.dp), + text = "링크 불러오는 중", + color = DoraColorTokens.G3, + style = DoraTypoTokens.caption1Normal, + ) + LottieLoader( + modifier = Modifier.size(24.dp), + lottieRes = R.raw.dot_loading, + iterations = Int.MAX_VALUE, + ) + } + } } } } diff --git a/feature/home/src/main/java/com/mashup/dorabangs/feature/home/HomeViewModel.kt b/feature/home/src/main/java/com/mashup/dorabangs/feature/home/HomeViewModel.kt index 79674ab8..75f27cb3 100644 --- a/feature/home/src/main/java/com/mashup/dorabangs/feature/home/HomeViewModel.kt +++ b/feature/home/src/main/java/com/mashup/dorabangs/feature/home/HomeViewModel.kt @@ -21,6 +21,7 @@ import com.mashup.dorabangs.domain.usecase.folder.GetFolderListUseCase import com.mashup.dorabangs.domain.usecase.folder.GetPostsFromFolderUseCase import com.mashup.dorabangs.domain.usecase.posts.ChangePostFolder import com.mashup.dorabangs.domain.usecase.posts.DeletePostUseCase +import com.mashup.dorabangs.domain.usecase.posts.GetLocalPostsUseCase import com.mashup.dorabangs.domain.usecase.posts.GetPostUseCase import com.mashup.dorabangs.domain.usecase.posts.GetPostsPageUseCase import com.mashup.dorabangs.domain.usecase.posts.GetUnReadPostsCountUseCase @@ -63,6 +64,7 @@ class HomeViewModel @Inject constructor( private val changePostFolderUseCase: ChangePostFolder, private val patchPostInfoUseCase: PatchPostInfoUseCase, private val getPostUseCase: GetPostUseCase, + private val getLocalPostsUseCase: GetLocalPostsUseCase, ) : ViewModel(), ContainerHost { override val container = container(HomeState()) @@ -559,9 +561,25 @@ class HomeViewModel @Inject constructor( return feedCardList } + fun loadCachedPosts() = viewModelScope.doraLaunch { + intent { + val posts = getLocalPostsUseCase(LOCAL_CACHED_POSTS_COUNT) + + if (state.folderList.isEmpty()) { + initFolderList() + } + + reduce { state.copy(postList = posts.items.toUIModel(state.folderList)) } + } + } + private fun List.toUIModel(folderList: List) = this.map { post -> val category = folderList.firstOrNull { folder -> folder.id == post.folderId }?.name.orEmpty() post.toUiModel(category) } + + companion object { + private const val LOCAL_CACHED_POSTS_COUNT = 10 + } }