Skip to content

Commit 2bfa994

Browse files
authored
Merge pull request #46 from YAPP-Github/BOOK-106-feature/#31
feat: 도서 검색 화면 구성 및 도서 검색 API 연동
2 parents b5c0190 + 8e4f243 commit 2bfa994

File tree

39 files changed

+1175
-27
lines changed

39 files changed

+1175
-27
lines changed

core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Modifier.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.ninecraft.booket.core.common.extensions
22

3-
import android.annotation.SuppressLint
43
import androidx.compose.foundation.clickable
54
import androidx.compose.foundation.interaction.MutableInteractionSource
65
import androidx.compose.material3.ripple
@@ -13,7 +12,6 @@ import com.ninecraft.booket.core.common.utils.MultipleEventsCutter
1312
import com.ninecraft.booket.core.common.utils.get
1413

1514
// https://stackoverflow.com/questions/66703448/how-to-disable-ripple-effect-when-clicking-in-jetpack-compose
16-
@SuppressLint("ModifierFactoryUnreferencedReceiver")
1715
inline fun Modifier.noRippleClickable(crossinline onClick: () -> Unit): Modifier = composed {
1816
clickable(
1917
indication = null,
@@ -23,7 +21,6 @@ inline fun Modifier.noRippleClickable(crossinline onClick: () -> Unit): Modifier
2321
}
2422
}
2523

26-
@Suppress("ModifierFactoryUnreferencedReceiver")
2724
fun Modifier.clickableSingle(
2825
enabled: Boolean = true,
2926
onClickLabel: String? = null,
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.ninecraft.booket.core.common.extensions
2+
3+
fun String.decodeHtmlEntities(): String {
4+
return this
5+
.replace("&lt;", "<")
6+
.replace("&gt;", ">")
7+
.replace("&amp;", "&")
8+
.replace("&quot;", "\"")
9+
.replace("&apos;", "'")
10+
.replace("&#x27;", "'")
11+
.replace("&#x2F;", "/")
12+
.replace("&#39;", "'")
13+
.replace("&nbsp;", " ")
14+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.ninecraft.booket.core.data.api.repository
2+
3+
import com.ninecraft.booket.core.model.BookDetailModel
4+
import com.ninecraft.booket.core.model.BookSearchModel
5+
6+
interface BookRepository {
7+
suspend fun searchBook(
8+
query: String,
9+
start: Int,
10+
): Result<BookSearchModel>
11+
12+
suspend fun getBookDetail(itemId: String): Result<BookDetailModel>
13+
}

core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/di/RepositoryModule.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package com.ninecraft.booket.core.data.impl.di
22

33
import com.ninecraft.booket.core.data.api.repository.AuthRepository
4+
import com.ninecraft.booket.core.data.api.repository.BookRepository
45
import com.ninecraft.booket.core.data.api.repository.UserRepository
56
import com.ninecraft.booket.core.data.impl.repository.DefaultAuthRepository
7+
import com.ninecraft.booket.core.data.impl.repository.DefaultBookRepository
68
import com.ninecraft.booket.core.data.impl.repository.DefaultUserRepository
79
import dagger.Binds
810
import dagger.Module
@@ -21,4 +23,8 @@ internal abstract class RepositoryModule {
2123
@Binds
2224
@Singleton
2325
abstract fun bindUserRepository(defaultUserRepository: DefaultUserRepository): UserRepository
26+
27+
@Binds
28+
@Singleton
29+
abstract fun bindBookRepository(defaultBookRepository: DefaultBookRepository): BookRepository
2430
}

core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/mapper/ResponseToModel.kt

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
package com.ninecraft.booket.core.data.impl.mapper
22

3+
import com.ninecraft.booket.core.common.extensions.decodeHtmlEntities
4+
import com.ninecraft.booket.core.model.BookDetailModel
5+
import com.ninecraft.booket.core.model.BookSearchModel
6+
import com.ninecraft.booket.core.model.BookSummaryModel
37
import com.ninecraft.booket.core.model.LoginModel
48
import com.ninecraft.booket.core.model.UserProfileModel
9+
import com.ninecraft.booket.core.network.response.BookDetailResponse
10+
import com.ninecraft.booket.core.network.response.BookSearchResponse
11+
import com.ninecraft.booket.core.network.response.BookSummary
512
import com.ninecraft.booket.core.network.response.LoginResponse
613
import com.ninecraft.booket.core.network.response.UserProfileResponse
714

@@ -20,3 +27,52 @@ internal fun UserProfileResponse.toModel(): UserProfileModel {
2027
provider = provider,
2128
)
2229
}
30+
31+
internal fun BookSearchResponse.toModel(): BookSearchModel {
32+
return BookSearchModel(
33+
version = version,
34+
title = title,
35+
link = link,
36+
pubDate = pubDate,
37+
totalResults = totalResults,
38+
startIndex = startIndex,
39+
itemsPerPage = itemsPerPage,
40+
query = query,
41+
searchCategoryId = searchCategoryId,
42+
searchCategoryName = searchCategoryName,
43+
books = books.map { it.toModel() },
44+
)
45+
}
46+
47+
internal fun BookSummary.toModel(): BookSummaryModel {
48+
return BookSummaryModel(
49+
isbn = isbn,
50+
title = title.decodeHtmlEntities(),
51+
author = author,
52+
publisher = publisher,
53+
coverImageUrl = coverImageUrl,
54+
)
55+
}
56+
57+
internal fun BookDetailResponse.toModel(): BookDetailModel {
58+
return BookDetailModel(
59+
version = version,
60+
title = title,
61+
link = link,
62+
author = author,
63+
pubDate = pubDate,
64+
description = description,
65+
isbn = isbn,
66+
isbn13 = isbn13,
67+
itemId = itemId,
68+
priceSales = priceSales,
69+
priceStandard = priceStandard,
70+
mallType = mallType,
71+
stockStatus = stockStatus,
72+
mileage = mileage,
73+
cover = cover,
74+
categoryId = categoryId,
75+
categoryName = categoryName,
76+
publisher = publisher,
77+
)
78+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.ninecraft.booket.core.data.impl.repository
2+
3+
import com.ninecraft.booket.core.common.utils.runSuspendCatching
4+
import com.ninecraft.booket.core.data.api.repository.BookRepository
5+
import com.ninecraft.booket.core.data.impl.mapper.toModel
6+
import com.ninecraft.booket.core.network.service.NoAuthService
7+
import javax.inject.Inject
8+
9+
internal class DefaultBookRepository @Inject constructor(
10+
private val service: NoAuthService,
11+
) : BookRepository {
12+
override suspend fun searchBook(
13+
query: String,
14+
start: Int,
15+
) = runSuspendCatching {
16+
service.searchBook(
17+
query = query,
18+
start = start,
19+
).toModel()
20+
}
21+
22+
override suspend fun getBookDetail(itemId: String) = runSuspendCatching {
23+
service.getBookDetail(itemId).toModel()
24+
}
25+
}

core/designsystem/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ dependencies {
1313
implementations(
1414
projects.core.common,
1515

16+
libs.compose.keyboard.state,
17+
libs.coil.compose,
1618
libs.logger,
19+
20+
libs.bundles.landscapist,
1721
)
1822
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.ninecraft.booket.core.designsystem.component
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.ui.Alignment
5+
import androidx.compose.ui.Modifier
6+
import androidx.compose.ui.graphics.painter.Painter
7+
import androidx.compose.ui.layout.ContentScale
8+
import androidx.compose.ui.res.painterResource
9+
import com.ninecraft.booket.core.designsystem.ComponentPreview
10+
import com.ninecraft.booket.core.designsystem.R
11+
import com.ninecraft.booket.core.designsystem.theme.ReedTheme
12+
import com.skydoves.landscapist.ImageOptions
13+
import com.skydoves.landscapist.coil.CoilImage
14+
import com.skydoves.landscapist.components.rememberImageComponent
15+
import com.skydoves.landscapist.placeholder.placeholder.PlaceholderPlugin
16+
17+
@Composable
18+
fun NetworkImage(
19+
imageUrl: String,
20+
contentDescription: String,
21+
modifier: Modifier = Modifier,
22+
placeholder: Painter? = null,
23+
contentScale: ContentScale = ContentScale.Crop,
24+
) {
25+
CoilImage(
26+
imageModel = { imageUrl },
27+
modifier = modifier,
28+
component = rememberImageComponent {
29+
+PlaceholderPlugin.Loading(placeholder)
30+
+PlaceholderPlugin.Failure(placeholder)
31+
},
32+
imageOptions = ImageOptions(
33+
contentScale = contentScale,
34+
alignment = Alignment.Center,
35+
contentDescription = contentDescription,
36+
),
37+
previewPlaceholder = placeholder,
38+
)
39+
}
40+
41+
@ComponentPreview
42+
@Composable
43+
private fun NetworkImagePreview() {
44+
ReedTheme {
45+
NetworkImage(
46+
imageUrl = "",
47+
contentDescription = "",
48+
placeholder = painterResource(R.drawable.ic_placeholder),
49+
)
50+
}
51+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.ninecraft.booket.core.designsystem.component
2+
3+
import androidx.compose.foundation.layout.PaddingValues
4+
import androidx.compose.foundation.layout.WindowInsets
5+
import androidx.compose.material3.Scaffold
6+
import androidx.compose.material3.ScaffoldDefaults
7+
import androidx.compose.runtime.Composable
8+
import androidx.compose.ui.Modifier
9+
import androidx.compose.ui.graphics.Color
10+
import com.ninecraft.booket.core.designsystem.theme.White
11+
import tech.thdev.compose.extensions.keyboard.state.foundation.keyboardHide
12+
13+
@Composable
14+
fun ReedScaffold(
15+
modifier: Modifier = Modifier,
16+
topBar: @Composable () -> Unit = {},
17+
bottomBar: @Composable () -> Unit = {},
18+
snackbarHost: @Composable () -> Unit = {},
19+
floatingActionButton: @Composable () -> Unit = {},
20+
containerColor: Color = White,
21+
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
22+
content: @Composable (PaddingValues) -> Unit,
23+
) {
24+
Scaffold(
25+
topBar = topBar,
26+
bottomBar = bottomBar,
27+
snackbarHost = snackbarHost,
28+
floatingActionButton = floatingActionButton,
29+
containerColor = containerColor,
30+
contentWindowInsets = contentWindowInsets,
31+
modifier = modifier.keyboardHide(),
32+
) { innerPadding ->
33+
content(innerPadding)
34+
}
35+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package com.ninecraft.booket.core.designsystem.component
2+
3+
import androidx.annotation.StringRes
4+
import androidx.compose.foundation.BorderStroke
5+
import androidx.compose.foundation.background
6+
import androidx.compose.foundation.border
7+
import androidx.compose.foundation.layout.Box
8+
import androidx.compose.foundation.layout.Row
9+
import androidx.compose.foundation.layout.Spacer
10+
import androidx.compose.foundation.layout.fillMaxWidth
11+
import androidx.compose.foundation.layout.height
12+
import androidx.compose.foundation.layout.padding
13+
import androidx.compose.foundation.layout.width
14+
import androidx.compose.foundation.shape.RoundedCornerShape
15+
import androidx.compose.foundation.text.BasicTextField
16+
import androidx.compose.foundation.text.KeyboardOptions
17+
import androidx.compose.foundation.text.input.TextFieldLineLimits
18+
import androidx.compose.foundation.text.input.TextFieldState
19+
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
20+
import androidx.compose.foundation.text.selection.TextSelectionColors
21+
import androidx.compose.material3.Icon
22+
import androidx.compose.material3.Text
23+
import androidx.compose.runtime.Composable
24+
import androidx.compose.runtime.CompositionLocalProvider
25+
import androidx.compose.ui.Alignment
26+
import androidx.compose.ui.Modifier
27+
import androidx.compose.ui.graphics.Color
28+
import androidx.compose.ui.graphics.vector.ImageVector
29+
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
30+
import androidx.compose.ui.res.stringResource
31+
import androidx.compose.ui.res.vectorResource
32+
import androidx.compose.ui.text.input.ImeAction
33+
import androidx.compose.ui.text.input.KeyboardType
34+
import androidx.compose.ui.unit.dp
35+
import com.ninecraft.booket.core.designsystem.ComponentPreview
36+
import com.ninecraft.booket.core.designsystem.R
37+
import com.ninecraft.booket.core.designsystem.theme.Green500
38+
import com.ninecraft.booket.core.designsystem.theme.ReedTheme
39+
40+
val reedTextSelectionColors = TextSelectionColors(
41+
handleColor = Green500,
42+
backgroundColor = Green500,
43+
)
44+
45+
@Composable
46+
fun ReedTextField(
47+
queryState: TextFieldState,
48+
@StringRes queryHintRes: Int,
49+
onSearch: (String) -> Unit,
50+
modifier: Modifier = Modifier,
51+
backgroundColor: Color = ReedTheme.colors.baseSecondary,
52+
textColor: Color = ReedTheme.colors.contentPrimary,
53+
cornerShape: RoundedCornerShape = RoundedCornerShape(ReedTheme.radius.sm),
54+
borderStroke: BorderStroke = BorderStroke(width = 1.dp, color = ReedTheme.colors.borderBrand),
55+
) {
56+
val keyboardController = LocalSoftwareKeyboardController.current
57+
58+
CompositionLocalProvider(LocalTextSelectionColors provides reedTextSelectionColors) {
59+
BasicTextField(
60+
state = queryState,
61+
modifier = Modifier.fillMaxWidth(),
62+
textStyle = ReedTheme.typography.body2Medium.copy(color = textColor),
63+
keyboardOptions = KeyboardOptions(
64+
keyboardType = KeyboardType.Text,
65+
imeAction = ImeAction.Search,
66+
),
67+
onKeyboardAction = {
68+
onSearch(queryState.text.toString())
69+
keyboardController?.hide()
70+
},
71+
lineLimits = TextFieldLineLimits.SingleLine,
72+
decorator = { innerTextField ->
73+
Row(
74+
modifier = modifier
75+
.background(color = backgroundColor, shape = cornerShape)
76+
.border(
77+
border = borderStroke,
78+
shape = cornerShape,
79+
)
80+
.padding(vertical = ReedTheme.spacing.spacing3),
81+
verticalAlignment = Alignment.CenterVertically,
82+
) {
83+
Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing4))
84+
Box {
85+
if (queryState.text.isEmpty()) {
86+
Text(
87+
text = stringResource(id = queryHintRes),
88+
color = ReedTheme.colors.contentTertiary,
89+
style = ReedTheme.typography.body2Regular,
90+
)
91+
}
92+
innerTextField()
93+
}
94+
Spacer(modifier = Modifier.weight(1f))
95+
Icon(
96+
imageVector = ImageVector.vectorResource(R.drawable.ic_search),
97+
contentDescription = "Search Icon",
98+
tint = ReedTheme.colors.contentBrand,
99+
)
100+
Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing4))
101+
}
102+
},
103+
)
104+
}
105+
}
106+
107+
@ComponentPreview
108+
@Composable
109+
private fun ReedTextFieldPreview() {
110+
ReedTheme {
111+
ReedTextField(
112+
queryState = TextFieldState("검색"),
113+
queryHintRes = R.string.search_book_hint,
114+
onSearch = {},
115+
modifier = Modifier
116+
.height(46.dp)
117+
.fillMaxWidth()
118+
.padding(horizontal = 20.dp),
119+
)
120+
}
121+
}

0 commit comments

Comments
 (0)