Skip to content

Commit c4d6c8b

Browse files
committed
[BOOK-151] feat: 최근 검색어 조회, 클릭 이벤트(검색, 삭제) 가능 구현
1 parent b00143f commit c4d6c8b

File tree

7 files changed

+129
-5
lines changed

7 files changed

+129
-5
lines changed

core/data/api/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ dependencies {
1212
implementations(
1313
projects.core.model,
1414

15+
libs.kotlinx.coroutines.core,
1516
libs.logger,
1617
)
1718
}

core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/BookRepository.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,18 @@ package com.ninecraft.booket.core.data.api.repository
33
import com.ninecraft.booket.core.model.BookDetailModel
44
import com.ninecraft.booket.core.model.BookSearchModel
55
import com.ninecraft.booket.core.model.BookUpsertModel
6+
import kotlinx.coroutines.flow.Flow
67

78
interface BookRepository {
9+
val recentSearches: Flow<List<String>>
10+
811
suspend fun searchBook(
912
query: String,
1013
start: Int,
1114
): Result<BookSearchModel>
1215

16+
suspend fun removeRecentSearch(query: String)
17+
1318
suspend fun getBookDetail(itemId: String): Result<BookDetailModel>
1419

1520
suspend fun upsertBook(

core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultBookRepository.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ internal class DefaultBookRepository @Inject constructor(
1212
private val service: ReedService,
1313
private val dataSource: RecentSearchDataSource,
1414
) : BookRepository {
15+
override val recentSearches = dataSource.recentSearches
16+
1517
override suspend fun searchBook(
1618
query: String,
1719
start: Int,
@@ -25,6 +27,11 @@ internal class DefaultBookRepository @Inject constructor(
2527
result
2628
}
2729

30+
override suspend fun removeRecentSearch(query: String) {
31+
dataSource.removeRecentSearch(query)
32+
}
33+
34+
2835
override suspend fun getBookDetail(itemId: String) = runSuspendCatching {
2936
service.getBookDetail(itemId).toModel()
3037
}

feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchPresenter.kt

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import com.ninecraft.booket.feature.screens.LoginScreen
1616
import com.ninecraft.booket.feature.screens.SearchScreen
1717
import com.orhanobut.logger.Logger
1818
import com.slack.circuit.codegen.annotations.CircuitInject
19+
import com.slack.circuit.retained.collectAsRetainedState
1920
import com.slack.circuit.retained.rememberRetained
2021
import com.slack.circuit.runtime.Navigator
2122
import com.slack.circuit.runtime.presenter.Presenter
@@ -24,12 +25,13 @@ import dagger.assisted.AssistedFactory
2425
import dagger.assisted.AssistedInject
2526
import dagger.hilt.android.components.ActivityRetainedComponent
2627
import kotlinx.collections.immutable.persistentListOf
28+
import kotlinx.collections.immutable.toImmutableList
2729
import kotlinx.collections.immutable.toPersistentList
2830
import kotlinx.coroutines.launch
2931

3032
class SearchPresenter @AssistedInject constructor(
3133
@Assisted private val navigator: Navigator,
32-
private val bookRepository: BookRepository,
34+
private val repository: BookRepository,
3335
) : Presenter<SearchUiState> {
3436
companion object {
3537
private const val PAGE_SIZE = 20
@@ -42,6 +44,7 @@ class SearchPresenter @AssistedInject constructor(
4244
var uiState by rememberRetained { mutableStateOf<UiState>(UiState.Idle) }
4345
var footerState by rememberRetained { mutableStateOf<FooterState>(FooterState.Idle) }
4446
val queryState = rememberTextFieldState()
47+
val recentSearches by repository.recentSearches.collectAsRetainedState(initial = emptyList())
4548
var searchResult by rememberRetained { mutableStateOf(BookSearchModel()) }
4649
var books by rememberRetained { mutableStateOf(persistentListOf<BookSummaryModel>()) }
4750
var currentStartIndex by rememberRetained { mutableIntStateOf(START_INDEX) }
@@ -60,7 +63,7 @@ class SearchPresenter @AssistedInject constructor(
6063
footerState = FooterState.Loading
6164
}
6265

63-
bookRepository.searchBook(query = query, start = startIndex)
66+
repository.searchBook(query = query, start = startIndex)
6467
.onSuccess { result ->
6568
searchResult = result
6669
books = if (startIndex == START_INDEX) {
@@ -92,7 +95,7 @@ class SearchPresenter @AssistedInject constructor(
9295

9396
fun upsertBook(bookIsbn: String, bookStatus: String) {
9497
scope.launch {
95-
bookRepository.upsertBook(bookIsbn, bookStatus)
98+
repository.upsertBook(bookIsbn, bookStatus)
9699
.onSuccess {
97100
selectedBookIsbn = ""
98101
selectedBookStatus = null
@@ -122,6 +125,16 @@ class SearchPresenter @AssistedInject constructor(
122125
navigator.pop()
123126
}
124127

128+
is SearchUiEvent.OnRecentSearchClick -> {
129+
searchBooks(query = event.query, startIndex = START_INDEX)
130+
}
131+
132+
is SearchUiEvent.OnRemoveSearchRemoveClick -> {
133+
scope.launch {
134+
repository.removeRecentSearch(query = event.query)
135+
}
136+
}
137+
125138
is SearchUiEvent.OnSearchClick -> {
126139
searchBooks(query = event.text, startIndex = START_INDEX)
127140
}
@@ -179,6 +192,7 @@ class SearchPresenter @AssistedInject constructor(
179192
uiState = uiState,
180193
footerState = footerState,
181194
queryState = queryState,
195+
recentSearches = recentSearches.toImmutableList(),
182196
searchResult = searchResult,
183197
books = books,
184198
startIndex = currentStartIndex,

feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchScreen.kt

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxSize
1010
import androidx.compose.foundation.layout.fillMaxWidth
1111
import androidx.compose.foundation.layout.height
1212
import androidx.compose.foundation.layout.padding
13+
import androidx.compose.foundation.lazy.LazyColumn
1314
import androidx.compose.material3.Button
1415
import androidx.compose.material3.CircularProgressIndicator
1516
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -28,12 +29,13 @@ import com.ninecraft.booket.core.designsystem.component.appbar.ReedBackTopAppBar
2829
import com.ninecraft.booket.core.designsystem.theme.ReedTheme
2930
import com.ninecraft.booket.core.designsystem.theme.White
3031
import com.ninecraft.booket.core.ui.component.ReedFullScreen
32+
import com.ninecraft.booket.feature.screens.SearchScreen
3133
import com.ninecraft.booket.feature.search.component.BookItem
3234
import com.ninecraft.booket.feature.search.component.BookRegisterBottomSheet
3335
import com.ninecraft.booket.feature.search.component.BookRegisterSuccessBottomSheet
3436
import com.ninecraft.booket.feature.search.component.InfinityLazyColumn
3537
import com.ninecraft.booket.feature.search.component.LoadStateFooter
36-
import com.ninecraft.booket.feature.screens.SearchScreen
38+
import com.ninecraft.booket.feature.search.component.SearchItem
3739
import com.slack.circuit.codegen.annotations.CircuitInject
3840
import dagger.hilt.android.components.ActivityRetainedComponent
3941
import kotlinx.collections.immutable.toImmutableList
@@ -122,7 +124,29 @@ internal fun SearchContent(
122124
}
123125

124126
is UiState.Idle -> {
125-
// TODO: 최근 검색어 노출
127+
LazyColumn {
128+
items(
129+
count = state.recentSearches.size,
130+
key = { index -> state.recentSearches[index] },
131+
) { index ->
132+
Column {
133+
SearchItem(
134+
query = state.recentSearches[index],
135+
onQueryClick = { keyword ->
136+
state.eventSink(SearchUiEvent.OnRecentSearchClick(keyword))
137+
},
138+
onRemoveIconClick = { keyword ->
139+
state.eventSink(SearchUiEvent.OnRemoveSearchRemoveClick(keyword))
140+
}
141+
)
142+
HorizontalDivider(
143+
modifier = Modifier.fillMaxWidth(),
144+
thickness = 1.dp,
145+
color = ReedTheme.colors.borderPrimary,
146+
)
147+
}
148+
}
149+
}
126150
}
127151

128152
is UiState.Success -> {

feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchUiState.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ data class SearchUiState(
2727
val uiState: UiState = UiState.Idle,
2828
val footerState: FooterState = FooterState.Idle,
2929
val queryState: TextFieldState = TextFieldState(""),
30+
val recentSearches: ImmutableList<String> = persistentListOf(),
3031
val searchResult: BookSearchModel = BookSearchModel(),
3132
val books: ImmutableList<BookSummaryModel> = persistentListOf(),
3233
val startIndex: Int = 0,
@@ -50,6 +51,8 @@ sealed interface SearchSideEffect {
5051

5152
sealed interface SearchUiEvent : CircuitUiEvent {
5253
data object OnBackClick : SearchUiEvent
54+
data class OnRecentSearchClick(val query: String) : SearchUiEvent
55+
data class OnRemoveSearchRemoveClick(val query: String) : SearchUiEvent
5356
data class OnSearchClick(val text: String) : SearchUiEvent
5457
data object OnClearClick : SearchUiEvent
5558
data class OnBookClick(val bookIsbn: String) : SearchUiEvent
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.ninecraft.booket.feature.search.component
2+
3+
import androidx.compose.foundation.clickable
4+
import androidx.compose.foundation.layout.Row
5+
import androidx.compose.foundation.layout.Spacer
6+
import androidx.compose.foundation.layout.fillMaxWidth
7+
import androidx.compose.foundation.layout.padding
8+
import androidx.compose.foundation.layout.size
9+
import androidx.compose.foundation.layout.width
10+
import androidx.compose.material3.Icon
11+
import androidx.compose.material3.Text
12+
import androidx.compose.runtime.Composable
13+
import androidx.compose.ui.Alignment
14+
import androidx.compose.ui.Modifier
15+
import androidx.compose.ui.graphics.vector.ImageVector
16+
import androidx.compose.ui.res.vectorResource
17+
import androidx.compose.ui.text.style.TextOverflow
18+
import androidx.compose.ui.unit.dp
19+
import com.ninecraft.booket.core.designsystem.ComponentPreview
20+
import com.ninecraft.booket.core.designsystem.theme.ReedTheme
21+
import com.ninecraft.booket.core.designsystem.R as designR
22+
23+
@Composable
24+
fun SearchItem(
25+
query: String,
26+
onQueryClick: (String) -> Unit,
27+
onRemoveIconClick: (String) -> Unit,
28+
modifier: Modifier = Modifier,
29+
) {
30+
Row(
31+
modifier = modifier
32+
.fillMaxWidth()
33+
.clickable { onQueryClick(query) }
34+
.padding(
35+
horizontal = ReedTheme.spacing.spacing6,
36+
vertical = ReedTheme.spacing.spacing4,
37+
),
38+
verticalAlignment = Alignment.CenterVertically,
39+
) {
40+
Text(
41+
text = query,
42+
color = ReedTheme.colors.contentSecondary,
43+
overflow = TextOverflow.Ellipsis,
44+
maxLines = 1,
45+
style = ReedTheme.typography.body1Medium,
46+
modifier = Modifier.weight(1f),
47+
)
48+
Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing3))
49+
Icon(
50+
imageVector = ImageVector.vectorResource(id = designR.drawable.ic_close),
51+
contentDescription = "Remove Icon",
52+
tint = ReedTheme.colors.contentSecondary,
53+
modifier = Modifier
54+
.size(18.dp)
55+
.clickable { onRemoveIconClick(query) },
56+
)
57+
}
58+
}
59+
60+
@ComponentPreview
61+
@Composable
62+
private fun SearchItemPreview() {
63+
ReedTheme {
64+
SearchItem(
65+
query = "최근 검색어 최근 검색어 최근 검색어 최근 검색어 최근 검색어",
66+
onQueryClick = {},
67+
onRemoveIconClick = {},
68+
)
69+
}
70+
}

0 commit comments

Comments
 (0)