Skip to content

Commit f6ac5a6

Browse files
authored
Merge pull request #55 from YAPP-Github/BOOK-145-feature/#50
feat: 도서 등록 플로우 구현
2 parents 756f2c1 + ec580a5 commit f6ac5a6

File tree

34 files changed

+765
-144
lines changed

34 files changed

+765
-144
lines changed

core/common/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ android {
1414
dependencies {
1515
implementations(
1616
projects.core.model,
17+
projects.core.network,
1718

1819
libs.logger,
1920
)
Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,77 @@
11
package com.ninecraft.booket.core.common.utils
22

3+
import com.ninecraft.booket.core.network.response.ErrorResponse
34
import com.orhanobut.logger.Logger
5+
import kotlinx.serialization.SerializationException
6+
import kotlinx.serialization.json.Json
47
import retrofit2.HttpException
8+
import java.io.IOException
59
import java.net.ConnectException
610
import java.net.SocketTimeoutException
711
import java.net.UnknownHostException
812

913
fun handleException(
1014
exception: Throwable,
15+
onError: (String) -> Unit,
1116
onLoginRequired: () -> Unit,
12-
onServerError: (String) -> Unit,
13-
onNetworkError: (String) -> Unit,
1417
) {
1518
when {
1619
exception is HttpException && exception.code() == 401 -> {
1720
onLoginRequired()
1821
}
1922

20-
exception is HttpException && exception.code() in 500..599 -> {
21-
onServerError("서버 오류가 발생했습니다.")
23+
exception is HttpException -> {
24+
val serverMessage = exception.parseErrorMessage()
25+
val message = serverMessage ?: getHttpErrorMessage(exception.code())
26+
Logger.e("HTTP ${exception.code()}: $message")
27+
onError(message)
2228
}
2329

24-
exception is UnknownHostException || exception is ConnectException -> {
25-
onNetworkError("네트워크 연결을 확인해주세요.")
26-
}
27-
28-
exception is SocketTimeoutException -> {
29-
onServerError("서버 응답 시간이 초과되었습니다.")
30+
exception.isNetworkError() -> {
31+
onError("네트워크 연결을 확인해주세요.")
3032
}
3133

3234
else -> {
33-
Logger.e(exception.message ?: "알 수 없는 오류가 발생했습니다.")
35+
val errorMessage = exception.message ?: "알 수 없는 오류가 발생했습니다"
36+
Logger.e(errorMessage)
37+
onError(errorMessage)
3438
}
3539
}
3640
}
41+
42+
@Suppress("TooGenericExceptionCaught")
43+
private fun HttpException.parseErrorMessage(): String? {
44+
return try {
45+
val errorBody = response()?.errorBody()?.string()
46+
if (errorBody.isNullOrBlank()) return null
47+
Json.decodeFromString<ErrorResponse>(errorBody).getErrorMessage()
48+
} catch (e: SerializationException) {
49+
Logger.e("JSON parsing failed: ${e.message}")
50+
null
51+
} catch (e: IOException) {
52+
Logger.e("Failed to read error body: ${e.message}")
53+
null
54+
} catch (e: Exception) {
55+
Logger.e("Unexpected error parsing response: ${e.message}")
56+
null
57+
}
58+
}
59+
60+
private fun getHttpErrorMessage(statusCode: Int): String {
61+
return when (statusCode) {
62+
400 -> "요청이 올바르지 않습니다"
63+
403 -> "접근 권한이 없습니다"
64+
404 -> "존재하지 않는 데이터입니다"
65+
429 -> "요청이 너무 많습니다. 잠시 후 다시 시도해주세요"
66+
in 400..499 -> "요청 처리 중 오류가 발생했습니다"
67+
in 500..599 -> "서버 오류가 발생했습니다"
68+
else -> "알 수 없는 오류가 발생했습니다"
69+
}
70+
}
71+
72+
private fun Throwable.isNetworkError(): Boolean {
73+
return this is UnknownHostException ||
74+
this is ConnectException ||
75+
this is SocketTimeoutException ||
76+
this is IOException
77+
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.ninecraft.booket.core.data.api.repository
22

33
import com.ninecraft.booket.core.model.BookDetailModel
44
import com.ninecraft.booket.core.model.BookSearchModel
5+
import com.ninecraft.booket.core.model.BookUpsertModel
56

67
interface BookRepository {
78
suspend fun searchBook(
@@ -10,4 +11,9 @@ interface BookRepository {
1011
): Result<BookSearchModel>
1112

1213
suspend fun getBookDetail(itemId: String): Result<BookDetailModel>
14+
15+
suspend fun upsertBook(
16+
bookIsbn: String,
17+
bookStatus: String,
18+
): Result<BookUpsertModel>
1319
}

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import com.ninecraft.booket.core.common.extensions.decodeHtmlEntities
44
import com.ninecraft.booket.core.model.BookDetailModel
55
import com.ninecraft.booket.core.model.BookSearchModel
66
import com.ninecraft.booket.core.model.BookSummaryModel
7+
import com.ninecraft.booket.core.model.BookUpsertModel
78
import com.ninecraft.booket.core.model.UserProfileModel
89
import com.ninecraft.booket.core.network.response.BookDetailResponse
910
import com.ninecraft.booket.core.network.response.BookSearchResponse
1011
import com.ninecraft.booket.core.network.response.BookSummary
12+
import com.ninecraft.booket.core.network.response.BookUpsertResponse
1113
import com.ninecraft.booket.core.network.response.UserProfileResponse
1214

1315
internal fun UserProfileResponse.toModel(): UserProfileModel {
@@ -67,3 +69,18 @@ internal fun BookDetailResponse.toModel(): BookDetailModel {
6769
publisher = publisher,
6870
)
6971
}
72+
73+
internal fun BookUpsertResponse.toModel(): BookUpsertModel {
74+
return BookUpsertModel(
75+
userBookId = userBookId,
76+
userId = userId,
77+
bookIsbn = bookIsbn,
78+
bookTitle = bookTitle,
79+
bookAuthor = bookAuthor,
80+
status = status,
81+
coverImageUrl = coverImageUrl,
82+
publisher = publisher,
83+
createdAt = createdAt,
84+
updatedAt = updatedAt,
85+
)
86+
}

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,17 @@ import com.ninecraft.booket.core.common.utils.runSuspendCatching
44
import com.ninecraft.booket.core.data.api.repository.AuthRepository
55
import com.ninecraft.booket.core.datastore.api.datasource.TokenPreferencesDataSource
66
import com.ninecraft.booket.core.network.request.LoginRequest
7-
import com.ninecraft.booket.core.network.service.AuthService
8-
import com.ninecraft.booket.core.network.service.NoAuthService
7+
import com.ninecraft.booket.core.network.service.ReedService
98
import javax.inject.Inject
109

1110
private const val KAKAO_PROVIDER_TYPE = "KAKAO"
1211

1312
internal class DefaultAuthRepository @Inject constructor(
14-
private val noAuthService: NoAuthService,
15-
private val authService: AuthService,
13+
private val service: ReedService,
1614
private val tokenDatasource: TokenPreferencesDataSource,
1715
) : AuthRepository {
1816
override suspend fun login(accessToken: String) = runSuspendCatching {
19-
val response = noAuthService.login(
17+
val response = service.login(
2018
LoginRequest(
2119
providerType = KAKAO_PROVIDER_TYPE,
2220
oauthToken = accessToken,
@@ -26,7 +24,7 @@ internal class DefaultAuthRepository @Inject constructor(
2624
}
2725

2826
override suspend fun logout() = runSuspendCatching {
29-
authService.logout()
27+
service.logout()
3028
clearTokens()
3129
}
3230

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ package com.ninecraft.booket.core.data.impl.repository
33
import com.ninecraft.booket.core.common.utils.runSuspendCatching
44
import com.ninecraft.booket.core.data.api.repository.BookRepository
55
import com.ninecraft.booket.core.data.impl.mapper.toModel
6-
import com.ninecraft.booket.core.network.service.NoAuthService
6+
import com.ninecraft.booket.core.network.request.BookUpsertRequest
7+
import com.ninecraft.booket.core.network.service.ReedService
78
import javax.inject.Inject
89

910
internal class DefaultBookRepository @Inject constructor(
10-
private val service: NoAuthService,
11+
private val service: ReedService,
1112
) : BookRepository {
1213
override suspend fun searchBook(
1314
query: String,
@@ -22,4 +23,8 @@ internal class DefaultBookRepository @Inject constructor(
2223
override suspend fun getBookDetail(itemId: String) = runSuspendCatching {
2324
service.getBookDetail(itemId).toModel()
2425
}
26+
27+
override suspend fun upsertBook(bookIsbn: String, bookStatus: String) = runSuspendCatching {
28+
service.upsertBook(BookUpsertRequest(bookIsbn, bookStatus)).toModel()
29+
}
2530
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ package com.ninecraft.booket.core.data.impl.repository
33
import com.ninecraft.booket.core.common.utils.runSuspendCatching
44
import com.ninecraft.booket.core.data.api.repository.UserRepository
55
import com.ninecraft.booket.core.data.impl.mapper.toModel
6-
import com.ninecraft.booket.core.network.service.AuthService
6+
import com.ninecraft.booket.core.network.service.ReedService
77
import javax.inject.Inject
88

99
internal class DefaultUserRepository @Inject constructor(
10-
private val authService: AuthService,
10+
private val service: ReedService,
1111
) : UserRepository {
1212
override suspend fun getUserProfile() = runSuspendCatching {
13-
authService.getUserProfile().toModel()
13+
service.getUserProfile().toModel()
1414
}
1515
}

core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/ReedTextField.kt

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import androidx.annotation.StringRes
44
import androidx.compose.foundation.BorderStroke
55
import androidx.compose.foundation.background
66
import androidx.compose.foundation.border
7+
import androidx.compose.foundation.clickable
78
import androidx.compose.foundation.layout.Box
89
import androidx.compose.foundation.layout.Row
910
import androidx.compose.foundation.layout.Spacer
@@ -47,6 +48,7 @@ fun ReedTextField(
4748
queryState: TextFieldState,
4849
@StringRes queryHintRes: Int,
4950
onSearch: (String) -> Unit,
51+
onClear: () -> Unit,
5052
modifier: Modifier = Modifier,
5153
backgroundColor: Color = ReedTheme.colors.baseSecondary,
5254
textColor: Color = ReedTheme.colors.contentPrimary,
@@ -58,7 +60,9 @@ fun ReedTextField(
5860
CompositionLocalProvider(LocalTextSelectionColors provides reedTextSelectionColors) {
5961
BasicTextField(
6062
state = queryState,
61-
modifier = Modifier.fillMaxWidth(),
63+
modifier = Modifier
64+
.fillMaxWidth()
65+
.height(50.dp),
6266
textStyle = ReedTheme.typography.body2Medium.copy(color = textColor),
6367
keyboardOptions = KeyboardOptions(
6468
keyboardType = KeyboardType.Text,
@@ -81,7 +85,7 @@ fun ReedTextField(
8185
verticalAlignment = Alignment.CenterVertically,
8286
) {
8387
Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing4))
84-
Box {
88+
Box(modifier = Modifier.weight(1f)) {
8589
if (queryState.text.isEmpty()) {
8690
Text(
8791
text = stringResource(id = queryHintRes),
@@ -91,10 +95,25 @@ fun ReedTextField(
9195
}
9296
innerTextField()
9397
}
94-
Spacer(modifier = Modifier.weight(1f))
98+
Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing2))
99+
if (queryState.text.toString().isNotEmpty()) {
100+
Icon(
101+
imageVector = ImageVector.vectorResource(R.drawable.ic_x_circle),
102+
contentDescription = "Clear Icon",
103+
modifier = Modifier.clickable {
104+
onClear()
105+
},
106+
tint = Color.Unspecified,
107+
)
108+
}
109+
Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing2))
95110
Icon(
96111
imageVector = ImageVector.vectorResource(R.drawable.ic_search),
97112
contentDescription = "Search Icon",
113+
modifier = Modifier.clickable {
114+
onSearch(queryState.text.toString())
115+
keyboardController?.hide()
116+
},
98117
tint = ReedTheme.colors.contentBrand,
99118
)
100119
Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing4))
@@ -112,6 +131,7 @@ private fun ReedTextFieldPreview() {
112131
queryState = TextFieldState("검색"),
113132
queryHintRes = R.string.search_book_hint,
114133
onSearch = {},
134+
onClear = {},
115135
modifier = Modifier
116136
.height(46.dp)
117137
.fillMaxWidth()

core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/bottomsheet/ReedBottomSheet.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import androidx.compose.material3.SheetValue
1010
import androidx.compose.material3.rememberModalBottomSheetState
1111
import androidx.compose.runtime.Composable
1212
import androidx.compose.ui.Modifier
13-
import androidx.compose.ui.tooling.preview.Preview
1413
import androidx.compose.ui.unit.dp
14+
import com.ninecraft.booket.core.designsystem.ComponentPreview
1515
import com.ninecraft.booket.core.designsystem.component.button.ReedButton
1616
import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle
1717
import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle
@@ -40,7 +40,7 @@ fun ReedBottomSheet(
4040
}
4141

4242
@OptIn(ExperimentalMaterial3Api::class)
43-
@Preview(showBackground = true)
43+
@ComponentPreview
4444
@Composable
4545
private fun ReedBottomSheetPreview() {
4646
val sheetState = SheetState(

core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ReedButton.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ import androidx.compose.runtime.getValue
2424
import androidx.compose.runtime.remember
2525
import androidx.compose.ui.Modifier
2626
import androidx.compose.ui.graphics.graphicsLayer
27-
import androidx.compose.ui.tooling.preview.Preview
2827
import androidx.compose.ui.unit.dp
2928
import com.ninecraft.booket.core.common.utils.MultipleEventsCutter
3029
import com.ninecraft.booket.core.common.utils.get
30+
import com.ninecraft.booket.core.designsystem.ComponentPreview
3131

3232
@Composable
3333
fun ReedButton(
@@ -97,7 +97,7 @@ fun ReedButton(
9797
}
9898
}
9999

100-
@Preview(showBackground = true)
100+
@ComponentPreview
101101
@Composable
102102
private fun ReedLargeButtonPreview() {
103103
Column(
@@ -271,7 +271,7 @@ private fun ReedLargeButtonPreview() {
271271
}
272272
}
273273

274-
@Preview(showBackground = true)
274+
@ComponentPreview
275275
@Composable
276276
private fun ReedMediumButtonPreview() {
277277
Column(
@@ -445,7 +445,7 @@ private fun ReedMediumButtonPreview() {
445445
}
446446
}
447447

448-
@Preview(showBackground = true)
448+
@ComponentPreview
449449
@Composable
450450
private fun ReedSmallButtonPreview() {
451451
Column(
@@ -619,7 +619,7 @@ private fun ReedSmallButtonPreview() {
619619
}
620620
}
621621

622-
@Preview(showBackground = true)
622+
@ComponentPreview
623623
@Composable
624624
private fun ReedButtonDisabledPreview() {
625625
Column(

0 commit comments

Comments
 (0)