Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading