Skip to content

Commit fbceb2a

Browse files
committed
[BOOK-272] feat: 등록한 도서 삭제 기능 구현
1 parent 71a931b commit fbceb2a

File tree

8 files changed

+213
-10
lines changed

8 files changed

+213
-10
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,5 @@ interface BookRepository {
4444

4545
suspend fun getSeedsStats(userBookId: String): Result<SeedModel>
4646

47-
suspend fun removeBook(userBookId: String): Result<Unit>
47+
suspend fun deleteBook(userBookId: String): Result<Unit>
4848
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ internal class DefaultBookRepository @Inject constructor(
6565
service.getSeedsStats(userBookId).toModel()
6666
}
6767

68-
override suspend fun removeBook(userBookId: String) = runSuspendCatching {
69-
service.removeBook(userBookId)
68+
override suspend fun deleteBook(userBookId: String) = runSuspendCatching {
69+
service.deleteBook(userBookId)
7070
}
7171
}

core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/ReedService.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ interface ReedService {
8181
): LibraryResponse
8282

8383
@DELETE("api/v1/books/my-library/{userBookId}")
84-
suspend fun removeBook(
84+
suspend fun deleteBook(
8585
@Path("userBookId") userBookId: String,
8686
)
8787

feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ class BookDetailPresenter @AssistedInject constructor(
7878
var isRecordSortBottomSheetVisible by rememberRetained { mutableStateOf(false) }
7979
var isRecordMenuBottomSheetVisible by rememberRetained { mutableStateOf(false) }
8080
var isRecordDeleteDialogVisible by rememberRetained { mutableStateOf(false) }
81+
var isDetailMenuBottomSheetVisible by rememberRetained { mutableStateOf(false) }
82+
var isBookDeleteDialogVisible by rememberRetained { mutableStateOf(false) }
8183
var sideEffect by rememberRetained { mutableStateOf<BookDetailSideEffect?>(null) }
8284

8385
@Suppress("TooGenericExceptionCaught")
@@ -212,6 +214,29 @@ class BookDetailPresenter @AssistedInject constructor(
212214
}
213215
}
214216

217+
fun deleteBook(userBookId: String, onSuccess: () -> Unit) {
218+
scope.launch {
219+
bookRepository.deleteBook(userBookId = userBookId)
220+
.onSuccess {
221+
onSuccess()
222+
}
223+
.onFailure { exception ->
224+
val handleErrorMessage = { message: String ->
225+
Logger.e(message)
226+
sideEffect = BookDetailSideEffect.ShowToast(message)
227+
}
228+
229+
handleException(
230+
exception = exception,
231+
onError = handleErrorMessage,
232+
onLoginRequired = {
233+
navigator.resetRoot(LoginScreen)
234+
},
235+
)
236+
}
237+
}
238+
}
239+
215240
LaunchedEffect(Unit) {
216241
initialLoad()
217242
}
@@ -297,7 +322,7 @@ class BookDetailPresenter @AssistedInject constructor(
297322
isRecordDeleteDialogVisible = true
298323
}
299324

300-
is BookDetailUiEvent.OnDelete -> {
325+
is BookDetailUiEvent.OnDeleteRecord -> {
301326
isRecordDeleteDialogVisible = false
302327
deleteRecord(
303328
readingRecordId = selectedRecordInfo.id,
@@ -313,6 +338,33 @@ class BookDetailPresenter @AssistedInject constructor(
313338
navigator.goTo(RecordDetailScreen(event.recordId))
314339
}
315340

341+
is BookDetailUiEvent.OnDetailMenuClick -> {
342+
isDetailMenuBottomSheetVisible = true
343+
}
344+
345+
is BookDetailUiEvent.OnDetailMenuBottomSheetDismiss -> {
346+
isDetailMenuBottomSheetVisible = false
347+
}
348+
349+
is BookDetailUiEvent.OnDeleteBookClick -> {
350+
isDetailMenuBottomSheetVisible = false
351+
isBookDeleteDialogVisible = true
352+
}
353+
354+
is BookDetailUiEvent.OnDeleteDialogDismiss -> {
355+
isBookDeleteDialogVisible = false
356+
}
357+
358+
is BookDetailUiEvent.OnDeleteBook -> {
359+
isBookDeleteDialogVisible = false
360+
deleteBook(
361+
userBookId = screen.userBookId,
362+
onSuccess = {
363+
navigator.pop()
364+
},
365+
)
366+
}
367+
316368
is BookDetailUiEvent.OnLoadMore -> {
317369
if (uiState != UiState.Loading && footerState !is FooterState.Loading && !isLastPage) {
318370
loadMoreReadingRecords(startIndex = currentStartIndex + 1)
@@ -342,6 +394,8 @@ class BookDetailPresenter @AssistedInject constructor(
342394
selectedRecordInfo = selectedRecordInfo,
343395
isRecordMenuBottomSheetVisible = isRecordMenuBottomSheetVisible,
344396
isRecordDeleteDialogVisible = isRecordDeleteDialogVisible,
397+
isDetailMenuBottomSheetVisible = isDetailMenuBottomSheetVisible,
398+
isBookDeleteDialogVisible = isBookDeleteDialogVisible,
345399
sideEffect = sideEffect,
346400
eventSink = ::handleEvent,
347401
)

feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,14 @@ import com.ninecraft.booket.core.model.BookDetailModel
4040
import com.ninecraft.booket.core.ui.ReedScaffold
4141
import com.ninecraft.booket.core.ui.component.InfinityLazyColumn
4242
import com.ninecraft.booket.core.ui.component.LoadStateFooter
43-
import com.ninecraft.booket.core.ui.component.ReedBackTopAppBar
4443
import com.ninecraft.booket.core.ui.component.ReedDialog
4544
import com.ninecraft.booket.core.ui.component.ReedErrorUi
45+
import com.ninecraft.booket.core.ui.component.ReedTopAppBar
4646
import com.ninecraft.booket.feature.detail.R
4747
import com.ninecraft.booket.feature.detail.book.component.BookItem
4848
import com.ninecraft.booket.feature.detail.book.component.BookUpdateBottomSheet
4949
import com.ninecraft.booket.feature.detail.book.component.CollectedSeeds
50+
import com.ninecraft.booket.feature.detail.book.component.DetailMenuBottomSheet
5051
import com.ninecraft.booket.feature.detail.book.component.ReadingRecordsHeader
5152
import com.ninecraft.booket.feature.detail.book.component.RecordItem
5253
import com.ninecraft.booket.feature.detail.book.component.RecordSortBottomSheet
@@ -152,14 +153,40 @@ internal fun BookDetailUi(
152153
title = stringResource(R.string.record_delete_dialog_title),
153154
confirmButtonText = stringResource(R.string.record_delete_dialog_delete),
154155
onConfirmRequest = {
155-
state.eventSink(BookDetailUiEvent.OnDelete)
156+
state.eventSink(BookDetailUiEvent.OnDeleteRecord)
156157
},
157158
dismissButtonText = stringResource(R.string.record_delete_dialog_cancel),
158159
onDismissRequest = {
159160
state.eventSink(BookDetailUiEvent.OnRecordDeleteDialogDismiss)
160161
},
161162
)
162163
}
164+
165+
if (state.isDetailMenuBottomSheetVisible) {
166+
DetailMenuBottomSheet(
167+
onDismissRequest = {
168+
state.eventSink(BookDetailUiEvent.OnDetailMenuBottomSheetDismiss)
169+
},
170+
sheetState = recordMenuBottomSheetState,
171+
onDeleteBookClick = {
172+
state.eventSink(BookDetailUiEvent.OnDeleteBookClick)
173+
},
174+
)
175+
}
176+
177+
if (state.isBookDeleteDialogVisible) {
178+
ReedDialog(
179+
title = stringResource(R.string.record_delete_dialog_title),
180+
confirmButtonText = stringResource(R.string.record_delete_dialog_delete),
181+
onConfirmRequest = {
182+
state.eventSink(BookDetailUiEvent.OnDeleteBook)
183+
},
184+
dismissButtonText = stringResource(R.string.record_delete_dialog_cancel),
185+
onDismissRequest = {
186+
state.eventSink(BookDetailUiEvent.OnDeleteDialogDismiss)
187+
},
188+
)
189+
}
163190
}
164191

165192
@Composable
@@ -191,11 +218,17 @@ internal fun BookDetailContent(
191218
},
192219
) {
193220
item {
194-
ReedBackTopAppBar(
221+
ReedTopAppBar(
195222
title = "",
196-
onBackClick = {
223+
startIconRes = designR.drawable.ic_chevron_left,
224+
startIconOnClick = {
197225
state.eventSink(BookDetailUiEvent.OnBackClick)
198226
},
227+
endIconRes = designR.drawable.ic_more_vertical,
228+
endIconDescription = "More Vertical Icon",
229+
endIconOnClick = {
230+
state.eventSink(BookDetailUiEvent.OnDetailMenuClick)
231+
},
199232
)
200233
}
201234

feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUiState.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ data class BookDetailUiState(
3939
val isRecordSortBottomSheetVisible: Boolean = false,
4040
val isRecordMenuBottomSheetVisible: Boolean = false,
4141
val isRecordDeleteDialogVisible: Boolean = false,
42+
val isDetailMenuBottomSheetVisible: Boolean = false,
43+
val isBookDeleteDialogVisible: Boolean = false,
4244
val sideEffect: BookDetailSideEffect? = null,
4345
val eventSink: (BookDetailUiEvent) -> Unit,
4446
) : CircuitUiState {
@@ -72,7 +74,12 @@ sealed interface BookDetailUiEvent : CircuitUiEvent {
7274
data object OnRecordDeleteDialogDismiss : BookDetailUiEvent
7375
data object OnEditRecordClick : BookDetailUiEvent
7476
data object OnDeleteRecordClick : BookDetailUiEvent
75-
data object OnDelete : BookDetailUiEvent
77+
data object OnDeleteRecord : BookDetailUiEvent
78+
data object OnDetailMenuClick : BookDetailUiEvent
79+
data object OnDetailMenuBottomSheetDismiss : BookDetailUiEvent
80+
data object OnDeleteBookClick : BookDetailUiEvent
81+
data object OnDeleteDialogDismiss : BookDetailUiEvent
82+
data object OnDeleteBook : BookDetailUiEvent
7683
data class OnRecordItemClick(val recordId: String) : BookDetailUiEvent
7784
data object OnLoadMore : BookDetailUiEvent
7885
data object OnRetryClick : BookDetailUiEvent
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package com.ninecraft.booket.feature.detail.book.component
2+
3+
import androidx.compose.foundation.layout.Column
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.width
9+
import androidx.compose.material3.ExperimentalMaterial3Api
10+
import androidx.compose.material3.Icon
11+
import androidx.compose.material3.SheetState
12+
import androidx.compose.material3.SheetValue
13+
import androidx.compose.material3.Text
14+
import androidx.compose.runtime.Composable
15+
import androidx.compose.ui.Alignment
16+
import androidx.compose.ui.Modifier
17+
import androidx.compose.ui.graphics.Color
18+
import androidx.compose.ui.graphics.vector.ImageVector
19+
import androidx.compose.ui.res.stringResource
20+
import androidx.compose.ui.res.vectorResource
21+
import com.ninecraft.booket.core.common.extensions.noRippleClickable
22+
import com.ninecraft.booket.core.designsystem.ComponentPreview
23+
import com.ninecraft.booket.core.designsystem.theme.ReedTheme
24+
import com.ninecraft.booket.core.ui.component.ReedBottomSheet
25+
import com.ninecraft.booket.feature.detail.R
26+
import com.ninecraft.booket.core.designsystem.R as designR
27+
28+
@OptIn(ExperimentalMaterial3Api::class)
29+
@Composable
30+
internal fun DetailMenuBottomSheet(
31+
onDismissRequest: () -> Unit,
32+
sheetState: SheetState,
33+
onDeleteBookClick: () -> Unit,
34+
modifier: Modifier = Modifier,
35+
) {
36+
ReedBottomSheet(
37+
onDismissRequest = {
38+
onDismissRequest()
39+
},
40+
sheetState = sheetState,
41+
) {
42+
Column(
43+
modifier = modifier
44+
.padding(top = ReedTheme.spacing.spacing5),
45+
horizontalAlignment = Alignment.CenterHorizontally,
46+
) {
47+
DetailMenuItem(
48+
iconResId = designR.drawable.ic_trash,
49+
iconDescription = "Trash Icon",
50+
label = stringResource(R.string.book_delete),
51+
color = ReedTheme.colors.contentError,
52+
onClick = { onDeleteBookClick() },
53+
)
54+
}
55+
}
56+
}
57+
58+
@Composable
59+
private fun DetailMenuItem(
60+
iconResId: Int,
61+
iconDescription: String,
62+
label: String,
63+
color: Color,
64+
onClick: () -> Unit,
65+
modifier: Modifier = Modifier,
66+
) {
67+
Row(
68+
modifier = modifier
69+
.fillMaxWidth()
70+
.noRippleClickable {
71+
onClick()
72+
}
73+
.padding(
74+
vertical = ReedTheme.spacing.spacing5,
75+
horizontal = ReedTheme.spacing.spacing6,
76+
),
77+
) {
78+
Icon(
79+
imageVector = ImageVector.vectorResource(iconResId),
80+
contentDescription = iconDescription,
81+
tint = color,
82+
)
83+
Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing3))
84+
Text(
85+
text = label,
86+
color = color,
87+
style = ReedTheme.typography.body1Medium,
88+
)
89+
}
90+
}
91+
92+
@OptIn(ExperimentalMaterial3Api::class)
93+
@ComponentPreview
94+
@Composable
95+
private fun ChoiceBottomSheetPreview() {
96+
val sheetState = SheetState(
97+
skipPartiallyExpanded = true,
98+
initialValue = SheetValue.Expanded,
99+
positionalThreshold = { 0f },
100+
velocityThreshold = { 0f },
101+
)
102+
103+
DetailMenuBottomSheet(
104+
onDismissRequest = {},
105+
sheetState = sheetState,
106+
onDeleteBookClick = {},
107+
)
108+
}

feature/detail/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@
1616
<string name="record_delete_dialog_title">삭제하면 기록을 복구할 수 없어요.\n정말 삭제하시겠어요?</string>
1717
<string name="record_delete_dialog_delete">삭제</string>
1818
<string name="record_delete_dialog_cancel">취소</string>
19+
<string name="book_delete">도서 삭제하기</string>
1920
</resources>

0 commit comments

Comments
 (0)