Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
444eb3b
[BOOK-167] chore: search 모듈 내에 도서 검색 과련 파일을 book 패키지로 분리
seoyoon513 Jul 29, 2025
e6d6673
[BOOK-167] feat: 내서재 검색 화면 기본 구성 및 navigation 연결
seoyoon513 Jul 29, 2025
9f9efe8
[BOOK-167] feat: ReedSearchTextField 파라미터 추가 및 기본값 수정
seoyoon513 Jul 29, 2025
c86f37b
[BOOK-167] feat: 내서재 검색 화면 1차 작업
seoyoon513 Jul 29, 2025
ca52cb6
[BOOK-167] feat: 내서재 검색 API 정의
seoyoon513 Aug 3, 2025
695a619
[BOOK-167] feat: 내서재 검색 API 연동
seoyoon513 Aug 3, 2025
88de4ec
[BOOK-167] feat: OnLoadMore, OnRetryClick 이벤트에서 공백 문자열 방지
seoyoon513 Aug 3, 2025
8a4a647
[BOOK-167] feat: 내서재 최근 검색어 DataStore 구축 및 RecentSearch를 도서(Book)/내서재…
seoyoon513 Aug 4, 2025
f8b52a9
[BOOK-167] feat: 내서재 최근 검색어 조회, 삭제 기능 구현
seoyoon513 Aug 4, 2025
764772b
[BOOK-167] feat: 도서 검색에서 쿼리 유효성 검사 추가 (빈문자열, 공백 차단)
seoyoon513 Aug 4, 2025
ee87a22
[BOOK-167] feat: 내서재 페이징 조회 PAGE_SIZE 변경
seoyoon513 Aug 4, 2025
345fe0a
[BOOK-167] chore: code style check success
seoyoon513 Aug 4, 2025
6bed0bc
[BOOK-167] chore: 공통 컴포넌트를 common으로 분리
seoyoon513 Aug 4, 2025
78b28d4
Merge branch 'develop' into BOOK-167-feature/#69
seoyoon513 Aug 4, 2025
82fde36
[BOOK-167] chore: 필드명 변경 (isbn -> isbn13)
seoyoon513 Aug 4, 2025
bc0d3d4
[BOOK-167] fix: 최근 검색어 선택 시 queryState 미반영으로 페이징 실패 문제 수정
seoyoon513 Aug 4, 2025
da74fff
[BOOK-167] feat: HandlingLibrarySearchSideEffect 추가
seoyoon513 Aug 4, 2025
9aeb51f
[BOOK-167] refactor: 필터 기반 조회 / 검색 기반 조회를 명확하게 구분할 수 있도록 함수 네이밍 수정
seoyoon513 Aug 4, 2025
868e30f
[BOOK-167] refactor: Search prefix를 BookSearch로 수정
seoyoon513 Aug 4, 2025
462fe4f
[BOOK-167] chore: 에러 메세지 수정
seoyoon513 Aug 4, 2025
276d695
[BOOK-167] chore: DataStore Name 수정
seoyoon513 Aug 4, 2025
1d6fb69
[BOOK-167] chore: 파라미터 네이밍 수정 isbn -> isbn13
seoyoon513 Aug 4, 2025
06231f5
[BOOK-167] chore: OnSearchClick에서 trim된 쿼리로 검색하도록 수정
seoyoon513 Aug 4, 2025
95283d8
[BOOK-167] chore: 파라미터 네이밍 수정 isbn -> isbn13
seoyoon513 Aug 4, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import kotlinx.coroutines.flow.Flow
import com.ninecraft.booket.core.model.LibraryModel

interface BookRepository {
val recentSearches: Flow<List<String>>
val bookRecentSearches: Flow<List<String>>
val libraryRecentSearches: Flow<List<String>>

suspend fun searchBook(
query: String,
start: Int,
): Result<BookSearchModel>

suspend fun removeRecentSearch(query: String)
suspend fun removeBookRecentSearch(query: String)

suspend fun getBookDetail(itemId: String): Result<BookDetailModel>

Expand All @@ -23,9 +24,17 @@ interface BookRepository {
bookStatus: String,
): Result<BookUpsertModel>

suspend fun getLibrary(
suspend fun filterLibraryBooks(
status: String?,
page: Int,
size: Int,
): Result<LibraryModel>

suspend fun searchLibraryBooks(
title: String,
page: Int,
size: Int,
): Result<LibraryModel>

suspend fun removeLibraryRecentSearch(query: String)
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ internal fun BookSearchResponse.toModel(): BookSearchModel {

internal fun BookSummary.toModel(): BookSummaryModel {
return BookSummaryModel(
isbn = isbn,
isbn13 = isbn13,
title = title.decodeHtmlEntities(),
author = author,
publisher = publisher,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@ package com.ninecraft.booket.core.data.impl.repository
import com.ninecraft.booket.core.common.utils.runSuspendCatching
import com.ninecraft.booket.core.data.api.repository.BookRepository
import com.ninecraft.booket.core.data.impl.mapper.toModel
import com.ninecraft.booket.core.datastore.api.datasource.RecentSearchDataSource
import com.ninecraft.booket.core.datastore.api.datasource.BookRecentSearchDataSource
import com.ninecraft.booket.core.datastore.api.datasource.LibraryRecentSearchDataSource
import com.ninecraft.booket.core.network.request.BookUpsertRequest
import com.ninecraft.booket.core.network.service.ReedService
import javax.inject.Inject

internal class DefaultBookRepository @Inject constructor(
private val service: ReedService,
private val dataSource: RecentSearchDataSource,
private val bookRecentSearchDataSource: BookRecentSearchDataSource,
private val libraryRecentSearchDataSource: LibraryRecentSearchDataSource,
) : BookRepository {
override val recentSearches = dataSource.recentSearches
override val bookRecentSearches = bookRecentSearchDataSource.recentSearches
override val libraryRecentSearches = libraryRecentSearchDataSource.recentSearches

override suspend fun searchBook(
query: String,
Expand All @@ -23,12 +26,12 @@ internal class DefaultBookRepository @Inject constructor(
start = start,
).toModel()

dataSource.addRecentSearch(query)
bookRecentSearchDataSource.addRecentSearch(query)
result
}

override suspend fun removeRecentSearch(query: String) {
dataSource.removeRecentSearch(query)
override suspend fun removeBookRecentSearch(query: String) {
bookRecentSearchDataSource.removeRecentSearch(query)
}

override suspend fun getBookDetail(itemId: String) = runSuspendCatching {
Expand All @@ -39,7 +42,18 @@ internal class DefaultBookRepository @Inject constructor(
service.upsertBook(BookUpsertRequest(bookIsbn, bookStatus)).toModel()
}

override suspend fun getLibrary(status: String?, page: Int, size: Int) = runSuspendCatching {
service.getLibrary(status, page, size).toModel()
override suspend fun filterLibraryBooks(status: String?, page: Int, size: Int) = runSuspendCatching {
service.getLibraryBooks(status, null, page, size).toModel()
}

override suspend fun searchLibraryBooks(title: String, page: Int, size: Int) = runSuspendCatching {
val result = service.getLibraryBooks(null, title, page, size).toModel()

libraryRecentSearchDataSource.addRecentSearch(title)
result
}

override suspend fun removeLibraryRecentSearch(query: String) {
libraryRecentSearchDataSource.removeRecentSearch(query)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.ninecraft.booket.core.datastore.api.datasource

interface BookRecentSearchDataSource : RecentSearchDataSource
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.ninecraft.booket.core.datastore.api.datasource

interface LibraryRecentSearchDataSource : RecentSearchDataSource
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import com.ninecraft.booket.core.datastore.api.datasource.RecentSearchDataSource
import com.ninecraft.booket.core.datastore.impl.di.RecentSearchDataStore
import com.ninecraft.booket.core.datastore.api.datasource.BookRecentSearchDataSource
import com.ninecraft.booket.core.datastore.impl.di.BookRecentSearchDataStore
import com.ninecraft.booket.core.datastore.impl.util.handleIOException
import com.orhanobut.logger.Logger
import kotlinx.coroutines.flow.Flow
Expand All @@ -14,14 +14,14 @@ import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import javax.inject.Inject

class DefaultRecentSearchDataSource @Inject constructor(
@RecentSearchDataStore private val dataStore: DataStore<Preferences>,
) : RecentSearchDataSource {
class DefaultBookRecentSearchDataSource @Inject constructor(
@BookRecentSearchDataStore private val dataStore: DataStore<Preferences>,
) : BookRecentSearchDataSource {
@Suppress("TooGenericExceptionCaught")
override val recentSearches: Flow<List<String>> = dataStore.data
.handleIOException()
.map { prefs ->
prefs[RECENT_SEARCHES]?.let { jsonString ->
prefs[BOOK_RECENT_SEARCHES]?.let { jsonString ->
try {
Json.decodeFromString<List<String>>(jsonString)
} catch (e: SerializationException) {
Expand All @@ -39,7 +39,7 @@ class DefaultRecentSearchDataSource @Inject constructor(
if (query.isBlank()) return

dataStore.edit { prefs ->
val currentSearches = prefs[RECENT_SEARCHES]?.let { jsonString ->
val currentSearches = prefs[BOOK_RECENT_SEARCHES]?.let { jsonString ->
try {
Json.decodeFromString<List<String>>(jsonString).toMutableList()
} catch (e: SerializationException) {
Expand All @@ -59,7 +59,7 @@ class DefaultRecentSearchDataSource @Inject constructor(
// 최근 10개만 유지
val limitedSearches = currentSearches.take(MAX_SEARCH_COUNT)
try {
prefs[RECENT_SEARCHES] = Json.encodeToString(limitedSearches)
prefs[BOOK_RECENT_SEARCHES] = Json.encodeToString(limitedSearches)
} catch (e: SerializationException) {
Logger.e(e, "Failed to serialize recent searches")
}
Expand All @@ -69,7 +69,7 @@ class DefaultRecentSearchDataSource @Inject constructor(
@Suppress("TooGenericExceptionCaught")
override suspend fun removeRecentSearch(query: String) {
dataStore.edit { prefs ->
val currentSearches = prefs[RECENT_SEARCHES]?.let { jsonString ->
val currentSearches = prefs[BOOK_RECENT_SEARCHES]?.let { jsonString ->
try {
Json.decodeFromString<List<String>>(jsonString).toMutableList()
} catch (e: SerializationException) {
Expand All @@ -83,7 +83,7 @@ class DefaultRecentSearchDataSource @Inject constructor(

currentSearches.remove(query)
try {
prefs[RECENT_SEARCHES] = Json.encodeToString(currentSearches)
prefs[BOOK_RECENT_SEARCHES] = Json.encodeToString(currentSearches)
} catch (e: SerializationException) {
Logger.e(e, "Failed to serialize recent searches after removal")
}
Expand All @@ -92,12 +92,12 @@ class DefaultRecentSearchDataSource @Inject constructor(

override suspend fun clearRecentSearches() {
dataStore.edit { prefs ->
prefs.remove(RECENT_SEARCHES)
prefs.remove(BOOK_RECENT_SEARCHES)
}
}

companion object {
private val RECENT_SEARCHES = stringPreferencesKey("RECENT_SEARCHES")
private val BOOK_RECENT_SEARCHES = stringPreferencesKey("BOOK_RECENT_SEARCHES")
private const val MAX_SEARCH_COUNT = 10
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.ninecraft.booket.core.datastore.impl.datasource

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import com.ninecraft.booket.core.datastore.api.datasource.LibraryRecentSearchDataSource
import com.ninecraft.booket.core.datastore.impl.di.LibraryRecentSearchDataStore
import com.ninecraft.booket.core.datastore.impl.util.handleIOException
import com.orhanobut.logger.Logger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import javax.inject.Inject

class DefaultLibraryRecentSearchDataSource @Inject constructor(
@LibraryRecentSearchDataStore private val dataStore: DataStore<Preferences>,
) : LibraryRecentSearchDataSource {
@Suppress("TooGenericExceptionCaught")
override val recentSearches: Flow<List<String>> = dataStore.data
.handleIOException()
.map { prefs ->
prefs[LIBRARY_RECENT_SEARCHES]?.let { jsonString ->
try {
Json.decodeFromString<List<String>>(jsonString)
} catch (e: SerializationException) {
Logger.e(e, "Failed to deserialize recent searches")
emptyList()
} catch (e: Exception) {
Logger.e(e, "Unexpected error while reading recent searches")
emptyList()
}
} ?: emptyList()
}

@Suppress("TooGenericExceptionCaught")
override suspend fun addRecentSearch(query: String) {
if (query.isBlank()) return

dataStore.edit { prefs ->
val currentSearches = prefs[LIBRARY_RECENT_SEARCHES]?.let { jsonString ->
try {
Json.decodeFromString<List<String>>(jsonString).toMutableList()
} catch (e: SerializationException) {
Logger.e(e, "Failed to deserialize recent searches for adding")
mutableListOf()
} catch (e: Exception) {
Logger.e(e, "Unexpected error while adding recent search")
mutableListOf()
}
} ?: mutableListOf()

// 기존에 있으면 제거 (upsert 로직)
currentSearches.remove(query)
// 맨 앞에 추가 (가장 최근 검색어)
currentSearches.add(0, query)

// 최근 10개만 유지
val limitedSearches = currentSearches.take(MAX_SEARCH_COUNT)
try {
prefs[LIBRARY_RECENT_SEARCHES] = Json.encodeToString(limitedSearches)
} catch (e: SerializationException) {
Logger.e(e, "Failed to serialize recent searches")
}
}
}

@Suppress("TooGenericExceptionCaught")
override suspend fun removeRecentSearch(query: String) {
dataStore.edit { prefs ->
val currentSearches = prefs[LIBRARY_RECENT_SEARCHES]?.let { jsonString ->
try {
Json.decodeFromString<List<String>>(jsonString).toMutableList()
} catch (e: SerializationException) {
Logger.e(e, "Failed to deserialize recent searches for removal")
mutableListOf()
} catch (e: Exception) {
Logger.e(e, "Unexpected error while removing recent search")
mutableListOf()
}
} ?: mutableListOf()

currentSearches.remove(query)
try {
prefs[LIBRARY_RECENT_SEARCHES] = Json.encodeToString(currentSearches)
} catch (e: SerializationException) {
Logger.e(e, "Failed to serialize recent searches after removal")
}
}
}

override suspend fun clearRecentSearches() {
dataStore.edit { prefs ->
prefs.remove(LIBRARY_RECENT_SEARCHES)
}
}

companion object {
private val LIBRARY_RECENT_SEARCHES = stringPreferencesKey("LIBRARY_RECENT_SEARCHES")
private const val MAX_SEARCH_COUNT = 10
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import com.ninecraft.booket.core.datastore.api.datasource.BookRecentSearchDataSource
import com.ninecraft.booket.core.datastore.api.datasource.LibraryRecentSearchDataSource
import com.ninecraft.booket.core.datastore.api.datasource.OnboardingDataSource
import com.ninecraft.booket.core.datastore.api.datasource.RecentSearchDataSource
import com.ninecraft.booket.core.datastore.api.datasource.TokenDataSource
import com.ninecraft.booket.core.datastore.impl.datasource.DefaultLibraryRecentSearchDataSource
import com.ninecraft.booket.core.datastore.impl.datasource.DefaultOnboardingDataSource
import com.ninecraft.booket.core.datastore.impl.datasource.DefaultRecentSearchDataSource
import com.ninecraft.booket.core.datastore.impl.datasource.DefaultBookRecentSearchDataSource
import com.ninecraft.booket.core.datastore.impl.datasource.DefaultTokenDataSource
import dagger.Binds
import dagger.Module
Expand All @@ -22,11 +24,13 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
object DataStoreModule {
private const val TOKEN_DATASTORE_NAME = "TOKENS_DATASTORE"
private const val RECENT_SEARCH_DATASTORE_NAME = "RECENT_SEARCH_DATASTORE"
private const val BOOK_RECENT_SEARCH_DATASTORE_NAME = "BOOK_RECENT_SEARCH_DATASTORE"
private const val LIBRARY_RECENT_SEARCH_DATASTORE_NAME = "LIBRARY_RECENT_SEARCH_DATASTORE"
private const val ONBOARDING_DATASTORE_NAME = "ONBOARDING_DATASTORE"

private val Context.tokenDataStore by preferencesDataStore(name = TOKEN_DATASTORE_NAME)
private val Context.recentSearchDataStore by preferencesDataStore(name = RECENT_SEARCH_DATASTORE_NAME)
private val Context.bookRecentSearchDataStore by preferencesDataStore(name = BOOK_RECENT_SEARCH_DATASTORE_NAME)
private val Context.libraryRecentSearchDataStore by preferencesDataStore(name = LIBRARY_RECENT_SEARCH_DATASTORE_NAME)
private val Context.onboardingDataStore by preferencesDataStore(name = ONBOARDING_DATASTORE_NAME)

@TokenDataStore
Expand All @@ -36,12 +40,19 @@ object DataStoreModule {
@ApplicationContext context: Context,
): DataStore<Preferences> = context.tokenDataStore

@RecentSearchDataStore
@BookRecentSearchDataStore
@Provides
@Singleton
fun provideRecentSearchDataStore(
fun provideBookRecentSearchDataStore(
@ApplicationContext context: Context,
): DataStore<Preferences> = context.recentSearchDataStore
): DataStore<Preferences> = context.bookRecentSearchDataStore

@LibraryRecentSearchDataStore
@Provides
@Singleton
fun provideLibraryRecentSearchDataStore(
@ApplicationContext context: Context,
): DataStore<Preferences> = context.libraryRecentSearchDataStore

@OnboardingDataStore
@Provides
Expand All @@ -63,9 +74,15 @@ abstract class DataStoreBindModule {

@Binds
@Singleton
abstract fun bindRecentSearchDataSource(
defaultRecentSearchDataSource: DefaultRecentSearchDataSource,
): RecentSearchDataSource
abstract fun bindBookRecentSearchDataSource(
defaultBookRecentSearchDataSource: DefaultBookRecentSearchDataSource,
): BookRecentSearchDataSource

@Binds
@Singleton
abstract fun bindLibraryRecentSearchDataSource(
defaultLibraryRecentSearchDataSource: DefaultLibraryRecentSearchDataSource,
): LibraryRecentSearchDataSource

@Binds
@Singleton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ annotation class TokenDataStore

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class RecentSearchDataStore
annotation class BookRecentSearchDataStore

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class LibraryRecentSearchDataStore

@Qualifier
@Retention(AnnotationRetention.BINARY)
Expand Down
Loading
Loading