Skip to content

Commit

Permalink
Merge pull request #179 from WithPeace/feat/#178-검색화면_data_레이어_추가
Browse files Browse the repository at this point in the history
Feat/#178 검색화면 data 레이어 추가
  • Loading branch information
rhkrwngud445 authored Jan 1, 2025
2 parents 4dd445e + 6c1f3ee commit ea756ae
Show file tree
Hide file tree
Showing 39 changed files with 933 additions and 44 deletions.
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ android {
}
packaging {
resources {
merges += "META-INF/LICENSE.md"
merges += "META-INF/LICENSE-notice.md"
excludes += "META-INF/LICENSE.md"
excludes += "META-INF/LICENSE-notice.md"
}
}

Expand Down
18 changes: 17 additions & 1 deletion app/src/main/java/com/withpeace/withpeace/navigation/NavHost.kt
Original file line number Diff line number Diff line change
Expand Up @@ -217,11 +217,27 @@ fun WithpeaceNavHost(
},
)
searchGraph(
onShowSnackBar = {},
onAuthExpired = {},
onBackButtonClick = {
navController.popBackStack()
},
onShowSnackBar = { onShowSnackBar(SnackbarState(it)) },
onNavigationSnackBar = {
onShowSnackBar(
SnackbarState(
it,
SnackbarType.Navigator(
actionName = "목록 보러가기",
action = {
navController.navigatePolicyBookmarks()
},
),
),
)
},
onPolicyClick = {
navController.navigateToPolicyDetail(policyId = it)
},
)
policyDetailNavGraph(
onShowSnackBar = { onShowSnackBar(SnackbarState(it)) },
Expand Down
1 change: 1 addition & 0 deletions core/data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ dependencies {
implementation(project(":core:imagestorage"))
implementation(project(":core:testing"))
implementation(project(":core:analytics"))
implementation(project(":core:database"))
implementation(libs.skydoves.sandwich)
implementation(libs.androidx.paging)
testImplementation(libs.androidx.paging.testing)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ package com.withpeace.withpeace.core.data.di
import com.withpeace.withpeace.core.data.repository.DefaultAppUpdateRepository
import com.withpeace.withpeace.core.data.repository.DefaultImageRepository
import com.withpeace.withpeace.core.data.repository.DefaultPostRepository
import com.withpeace.withpeace.core.data.repository.DefaultRecentSearchKeywordRepository
import com.withpeace.withpeace.core.data.repository.DefaultTokenRepository
import com.withpeace.withpeace.core.data.repository.DefaultUserRepository
import com.withpeace.withpeace.core.data.repository.DefaultYouthPolicyRepository
import com.withpeace.withpeace.core.domain.repository.AppUpdateRepository
import com.withpeace.withpeace.core.domain.repository.ImageRepository
import com.withpeace.withpeace.core.domain.repository.PostRepository
import com.withpeace.withpeace.core.domain.repository.RecentSearchKeywordRepository
import com.withpeace.withpeace.core.domain.repository.TokenRepository
import com.withpeace.withpeace.core.domain.repository.UserRepository
import com.withpeace.withpeace.core.domain.repository.YouthPolicyRepository
Expand All @@ -17,8 +19,6 @@ import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.scopes.ViewModelScoped
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(ViewModelComponent::class)
Expand All @@ -42,6 +42,13 @@ interface RepositoryModule {
defaultUserRepository: DefaultUserRepository,
): UserRepository

@Binds
@ViewModelScoped
fun bindsRecentSearchKeywordRepository(
defaultRecentSearchKeywordRepository: DefaultRecentSearchKeywordRepository,
): RecentSearchKeywordRepository


@Binds
@ViewModelScoped
fun bindsYouthPolicyRepository(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.withpeace.withpeace.core.data.mapper

import com.withpeace.withpeace.core.database.SearchKeywordEntity
import com.withpeace.withpeace.core.domain.model.search.SearchKeyword

fun SearchKeywordEntity.toDomain(): SearchKeyword {
return SearchKeyword(keyword)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.withpeace.withpeace.core.data.paging

import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.skydoves.sandwich.ApiResponse
import com.withpeace.withpeace.core.data.mapper.youthpolicy.toDomain
import com.withpeace.withpeace.core.domain.model.error.CheonghaError
import com.withpeace.withpeace.core.domain.model.error.NoSearchResultException
import com.withpeace.withpeace.core.domain.model.policy.YouthPolicy
import com.withpeace.withpeace.core.domain.repository.UserRepository
import com.withpeace.withpeace.core.network.di.service.YouthPolicyService

class PolicySearchPagingSource(
private val pageSize: Int,
private val youthPolicyService: YouthPolicyService,
private val keyword: String,
private val onError: suspend (CheonghaError) -> Unit,
private val userRepository: UserRepository,
private val onReceiveTotalCount: suspend (Int) -> Unit,
) : PagingSource<Int, Pair<Int, YouthPolicy>>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Pair<Int, YouthPolicy>> {
val pageIndex = params.key ?: 1
val response = youthPolicyService.search(
keyword = keyword,
pageSize = params.loadSize,
pageIndex = pageIndex,
)

if (response is ApiResponse.Success) {
val successResponse = (response).data
onReceiveTotalCount(successResponse.data.totalCount)
if (response.data.data.totalCount == 0) {
return LoadResult.Error(NoSearchResultException())
}
return LoadResult.Page(
data = successResponse.data.policies.map {
Pair(
successResponse.data.totalCount,
it.toDomain(),
)
},
prevKey = if (pageIndex == STARTING_PAGE_INDEX) null else pageIndex - 1,
nextKey = if (successResponse.data.policies.isEmpty()) null else pageIndex + (params.loadSize / pageSize),
)
} else {
return LoadResult.Error(IllegalStateException("api state error"))
}
}

override fun getRefreshKey(state: PagingState<Int, Pair<Int, YouthPolicy>>): Int? { // 현재 포지션에서 Refresh pageKey 설정
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}

companion object {
private const val STARTING_PAGE_INDEX = 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.withpeace.withpeace.core.data.repository

import com.withpeace.withpeace.core.data.mapper.toDomain
import com.withpeace.withpeace.core.database.SearchKeywordDao
import com.withpeace.withpeace.core.database.SearchKeywordEntity
import com.withpeace.withpeace.core.domain.model.search.SearchKeyword
import com.withpeace.withpeace.core.domain.repository.RecentSearchKeywordRepository
import javax.inject.Inject

class DefaultRecentSearchKeywordRepository @Inject constructor(
private val searchKeywordDao: SearchKeywordDao,
) : RecentSearchKeywordRepository {
override suspend fun insertKeyword(keyword: SearchKeyword) {
searchKeywordDao.insertKeyword(SearchKeywordEntity(keyword = keyword.value))
}

override suspend fun getAllKeywords(): List<SearchKeyword> {
return searchKeywordDao.getAllKeywords().map { it.toDomain() }
}

override suspend fun deleteKeyword(keyword: SearchKeyword) {
searchKeywordDao.deleteKeyword(
SearchKeywordEntity(
keyword = keyword.value
),
)
}

override suspend fun clearAllKeywords() {
searchKeywordDao.clearAllKeywords()
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.withpeace.withpeace.core.data.repository

import android.util.Log
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.skydoves.sandwich.suspendMapSuccess
import com.withpeace.withpeace.core.data.mapper.youthpolicy.toDomain
import com.withpeace.withpeace.core.data.paging.PolicySearchPagingSource
import com.withpeace.withpeace.core.data.paging.YouthPolicyPagingSource
import com.withpeace.withpeace.core.data.util.handleApiFailure
import com.withpeace.withpeace.core.domain.model.error.CheonghaError
Expand All @@ -14,6 +16,7 @@ import com.withpeace.withpeace.core.domain.model.policy.BookmarkedPolicy
import com.withpeace.withpeace.core.domain.model.policy.PolicyFilters
import com.withpeace.withpeace.core.domain.model.policy.YouthPolicy
import com.withpeace.withpeace.core.domain.model.policy.YouthPolicyDetail
import com.withpeace.withpeace.core.domain.model.search.SearchKeyword
import com.withpeace.withpeace.core.domain.repository.UserRepository
import com.withpeace.withpeace.core.domain.repository.YouthPolicyRepository
import com.withpeace.withpeace.core.network.di.service.YouthPolicyService
Expand Down Expand Up @@ -102,6 +105,26 @@ class DefaultYouthPolicyRepository @Inject constructor(
}
}

override fun search(
searchKeyword: SearchKeyword,
onError: suspend (CheonghaError) -> Unit,
onReceiveTotalCount: (Int) -> Unit,
): Flow<PagingData<Pair<Int, YouthPolicy>>> {
return Pager(
config = PagingConfig(PAGE_SIZE),
pagingSourceFactory = {
PolicySearchPagingSource(
keyword = searchKeyword.value,
youthPolicyService = youthPolicyService,
onError = onError,
userRepository = userRepository,
pageSize = PAGE_SIZE,
onReceiveTotalCount = onReceiveTotalCount
)
},
).flow
}

private suspend fun onErrorWithAuthExpired(
it: ResponseError,
onError: suspend (CheonghaError) -> Unit,
Expand Down
1 change: 1 addition & 0 deletions core/database/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
15 changes: 15 additions & 0 deletions core/database/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
plugins {
id("com.android.library")
id("convention.android.base")
id("convention.android.hilt")
}

android {
namespace = "com.withpeace.withpeace.core.database"
}

dependencies {
implementation(libs.room.ktx)
implementation(libs.room.runtime)
kapt (libs.room.compiler)
}
Empty file.
21 changes: 21 additions & 0 deletions core/database/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.withpeace.withpeace.core.database

import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4

import org.junit.Test
import org.junit.runner.RunWith

import org.junit.Assert.*

/**
* 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("com.withpeace.withpeace.core.database.test", appContext.packageName)
}
}
4 changes: 4 additions & 0 deletions core/database/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.withpeace.withpeace.core.database

import androidx.room.Database
import androidx.room.RoomDatabase

@Database(entities = [SearchKeywordEntity::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun searchKeywordDao(): SearchKeywordDao
}



Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.withpeace.withpeace.core.database

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 javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {

@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"app_database",
).build()
}

@Provides
fun provideSearchKeywordDao(database: AppDatabase): SearchKeywordDao {
return database.searchKeywordDao()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.withpeace.withpeace.core.database

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query

@Dao
interface SearchKeywordDao {

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertKeyword(keyword: SearchKeywordEntity)

@Query("SELECT * FROM recent_search_keywords ORDER BY timestamp DESC LIMIT 8")
suspend fun getAllKeywords(): List<SearchKeywordEntity>

@Delete
suspend fun deleteKeyword(keyword: SearchKeywordEntity)

@Query("DELETE FROM recent_search_keywords WHERE keyword = :keyword")
suspend fun deleteKeywordByValue(keyword: String)

@Query("DELETE FROM recent_search_keywords")
suspend fun clearAllKeywords()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.withpeace.withpeace.core.database

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "recent_search_keywords")
data class SearchKeywordEntity(
@PrimaryKey(autoGenerate = false) val keyword: String,
val timestamp: Long = System.currentTimeMillis(), // 저장 시각
)
Loading

0 comments on commit ea756ae

Please sign in to comment.