Skip to content

Commit 1e15367

Browse files
authored
Merge pull request #149 from YAPP-Github/BOOK-265-feature/#145
feat: 독서 기록 수정 및 삭제 기능 구현
2 parents 96abbc7 + 977d408 commit 1e15367

File tree

34 files changed

+1631
-175
lines changed

34 files changed

+1631
-175
lines changed

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ dependencies {
8888
projects.feature.settings,
8989
projects.feature.splash,
9090
projects.feature.webview,
91+
projects.feature.edit,
9192

9293
libs.androidx.activity.compose,
9394
libs.androidx.startup,

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

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

3+
import com.ninecraft.booket.core.model.ReadingRecordModel
34
import com.ninecraft.booket.core.model.RecordRegisterModel
45
import com.ninecraft.booket.core.model.ReadingRecordsModel
56
import com.ninecraft.booket.core.model.RecordDetailModel
@@ -23,4 +24,16 @@ interface RecordRepository {
2324
suspend fun getRecordDetail(
2425
readingRecordId: String,
2526
): Result<RecordDetailModel>
27+
28+
suspend fun editRecord(
29+
readingRecordId: String,
30+
pageNumber: Int,
31+
quote: String,
32+
emotionTags: List<String>,
33+
review: String,
34+
): Result<ReadingRecordModel>
35+
36+
suspend fun deleteRecord(
37+
readingRecordId: String,
38+
): Result<Unit>
2639
}

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ 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.RecordRepository
55
import com.ninecraft.booket.core.data.impl.mapper.toModel
6+
import com.ninecraft.booket.core.model.ReadingRecordModel
67
import com.ninecraft.booket.core.network.request.RecordRegisterRequest
78
import com.ninecraft.booket.core.network.service.ReedService
89
import javax.inject.Inject
@@ -32,4 +33,18 @@ class DefaultRecordRepository @Inject constructor(
3233
override suspend fun getRecordDetail(readingRecordId: String) = runSuspendCatching {
3334
service.getRecordDetail(readingRecordId).toModel()
3435
}
36+
37+
override suspend fun editRecord(
38+
readingRecordId: String,
39+
pageNumber: Int,
40+
quote: String,
41+
emotionTags: List<String>,
42+
review: String,
43+
): Result<ReadingRecordModel> = runSuspendCatching {
44+
service.editRecord(readingRecordId, RecordRegisterRequest(pageNumber, quote, emotionTags, review)).toModel()
45+
}
46+
47+
override suspend fun deleteRecord(readingRecordId: String): Result<Unit> = runSuspendCatching {
48+
service.deleteRecord(readingRecordId)
49+
}
3550
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="24"
5+
android:viewportHeight="24">
6+
<path
7+
android:pathData="M21,15V19C21,19.53 20.789,20.039 20.414,20.414C20.039,20.789 19.53,21 19,21H5C4.47,21 3.961,20.789 3.586,20.414C3.211,20.039 3,19.53 3,19V15M7,10L12,15M12,15L17,10M12,15V3"
8+
android:strokeLineJoin="round"
9+
android:strokeWidth="1.5"
10+
android:fillColor="#00000000"
11+
android:strokeColor="#181D27"
12+
android:strokeLineCap="round"/>
13+
</vector>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="24"
5+
android:viewportHeight="24">
6+
<path
7+
android:pathData="M4,12V20C4,20.53 4.211,21.039 4.586,21.414C4.961,21.789 5.47,22 6,22H18C18.53,22 19.039,21.789 19.414,21.414C19.789,21.039 20,20.53 20,20V12M16,6L12,2M12,2L8,6M12,2V15"
8+
android:strokeLineJoin="round"
9+
android:strokeWidth="1.5"
10+
android:fillColor="#00000000"
11+
android:strokeColor="#181D27"
12+
android:strokeLineCap="round"/>
13+
</vector>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="24"
5+
android:viewportHeight="24">
6+
<path
7+
android:pathData="M8.684,13.342C8.886,12.938 9,12.482 9,12C9,11.518 8.886,11.062 8.684,10.658M8.684,13.342C8.191,14.325 7.174,15 6,15C4.343,15 3,13.657 3,12C3,10.343 4.343,9 6,9C7.174,9 8.191,9.675 8.684,10.658M8.684,13.342L15.316,16.658M8.684,10.658L15.316,7.342M15.316,16.658C15.114,17.062 15,17.518 15,18C15,19.657 16.343,21 18,21C19.657,21 21,19.657 21,18C21,16.343 19.657,15 18,15C16.826,15 15.809,15.675 15.316,16.658ZM15.316,7.342C15.809,8.325 16.826,9 18,9C19.657,9 21,7.657 21,6C21,4.343 19.657,3 18,3C16.343,3 15,4.343 15,6C15,6.482 15.114,6.938 15.316,7.342Z"
8+
android:strokeLineJoin="round"
9+
android:strokeWidth="1.5"
10+
android:fillColor="#00000000"
11+
android:strokeColor="#000000"
12+
android:strokeLineCap="round"/>
13+
</vector>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="24"
5+
android:viewportHeight="24">
6+
<path
7+
android:pathData="M3,6H5M5,6H21M5,6V20C5,20.53 5.211,21.039 5.586,21.414C5.961,21.789 6.47,22 7,22H17C17.53,22 18.039,21.789 18.414,21.414C18.789,21.039 19,20.53 19,20V6H5ZM8,6V4C8,3.47 8.211,2.961 8.586,2.586C8.961,2.211 9.47,2 10,2H14C14.53,2 15.039,2.211 15.414,2.586C15.789,2.961 16,3.47 16,4V6M10,11V17M14,11V17"
8+
android:strokeLineJoin="round"
9+
android:strokeWidth="1.5"
10+
android:fillColor="#00000000"
11+
android:strokeColor="#181D27"
12+
android:strokeLineCap="round"/>
13+
</vector>

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import com.ninecraft.booket.core.network.response.BookUpsertResponse
1111
import com.ninecraft.booket.core.network.response.HomeResponse
1212
import com.ninecraft.booket.core.network.response.LibraryResponse
1313
import com.ninecraft.booket.core.network.response.LoginResponse
14+
import com.ninecraft.booket.core.network.response.ReadingRecord
1415
import com.ninecraft.booket.core.network.response.ReadingRecordsResponse
1516
import com.ninecraft.booket.core.network.response.RecordDetailResponse
1617
import com.ninecraft.booket.core.network.response.RecordRegisterResponse
@@ -21,6 +22,7 @@ import com.ninecraft.booket.core.network.response.UserProfileResponse
2122
import retrofit2.http.Body
2223
import retrofit2.http.DELETE
2324
import retrofit2.http.GET
25+
import retrofit2.http.PATCH
2426
import retrofit2.http.POST
2527
import retrofit2.http.PUT
2628
import retrofit2.http.Path
@@ -103,6 +105,17 @@ interface ReedService {
103105
@Path("readingRecordId") readingRecordId: String,
104106
): RecordDetailResponse
105107

108+
@PATCH("api/v1/reading-records/{readingRecordId}")
109+
suspend fun editRecord(
110+
@Path("readingRecordId") readingRecordId: String,
111+
@Body recordRegisterRequest: RecordRegisterRequest,
112+
): ReadingRecord
113+
114+
@DELETE("api/v1/reading-records/{readingRecordId}")
115+
suspend fun deleteRecord(
116+
@Path("readingRecordId") readingRecordId: String,
117+
)
118+
106119
// Home (auth required)
107120
@GET("api/v1/home")
108121
suspend fun getHome(

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

Lines changed: 121 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ import com.ninecraft.booket.core.ui.component.FooterState
2020
import com.ninecraft.booket.feature.screens.BookDetailScreen
2121
import com.ninecraft.booket.feature.screens.LoginScreen
2222
import com.ninecraft.booket.feature.screens.RecordDetailScreen
23+
import com.ninecraft.booket.feature.screens.RecordEditScreen
2324
import com.ninecraft.booket.feature.screens.RecordScreen
25+
import com.ninecraft.booket.feature.screens.arguments.RecordEditArgs
2426
import com.orhanobut.logger.Logger
2527
import com.slack.circuit.codegen.annotations.CircuitInject
2628
import com.slack.circuit.retained.rememberRetained
@@ -71,57 +73,62 @@ class BookDetailPresenter @AssistedInject constructor(
7173
var currentBookStatus by rememberRetained { mutableStateOf(BookStatus.READING) }
7274
var selectedBookStatus by rememberRetained { mutableStateOf(BookStatus.READING) }
7375
var currentRecordSort by rememberRetained { mutableStateOf(RecordSort.PAGE_NUMBER_ASC) }
76+
var selectedRecordInfo by rememberRetained { mutableStateOf(ReadingRecordModel()) }
7477
var isBookUpdateBottomSheetVisible by rememberRetained { mutableStateOf(false) }
7578
var isRecordSortBottomSheetVisible by rememberRetained { mutableStateOf(false) }
79+
var isRecordMenuBottomSheetVisible by rememberRetained { mutableStateOf(false) }
80+
var isRecordDeleteDialogVisible by rememberRetained { mutableStateOf(false) }
7681
var sideEffect by rememberRetained { mutableStateOf<BookDetailSideEffect?>(null) }
7782

7883
@Suppress("TooGenericExceptionCaught")
79-
suspend fun initialLoad() {
84+
fun initialLoad() {
8085
uiState = UiState.Loading
8186

82-
try {
83-
coroutineScope {
84-
val bookDetailDef = async { bookRepository.getBookDetail(screen.isbn13).getOrThrow() }
85-
val seedsDef = async { bookRepository.getSeedsStats(screen.userBookId).getOrThrow() }
86-
val readingRecordsDef = async {
87-
recordRepository.getReadingRecords(
88-
userBookId = screen.userBookId,
89-
sort = currentRecordSort.value,
90-
page = START_INDEX,
91-
size = PAGE_SIZE,
92-
).getOrThrow()
93-
}
94-
val detail = bookDetailDef.await()
95-
val seeds = seedsDef.await()
96-
val records = readingRecordsDef.await()
87+
scope.launch {
88+
try {
89+
coroutineScope {
90+
val bookDetailDeferred = async { bookRepository.getBookDetail(screen.isbn13).getOrThrow() }
91+
val seedsDeferred = async { bookRepository.getSeedsStats(screen.userBookId).getOrThrow() }
92+
val readingRecordsDeferred = async {
93+
recordRepository.getReadingRecords(
94+
userBookId = screen.userBookId,
95+
sort = currentRecordSort.value,
96+
page = START_INDEX,
97+
size = PAGE_SIZE,
98+
).getOrThrow()
99+
}
100+
val detail = bookDetailDeferred.await()
101+
val seeds = seedsDeferred.await()
102+
val records = readingRecordsDeferred.await()
97103

98-
bookDetail = detail
99-
currentBookStatus = BookStatus.fromValue(detail.userBookStatus) ?: BookStatus.BEFORE_READING
100-
selectedBookStatus = currentBookStatus
101-
seedsStates = seeds.categories.toImmutableList()
102-
readingRecords = records.readingRecords.toPersistentList()
103-
readingRecordsTotalCount = records.totalResults
104+
bookDetail = detail
105+
currentBookStatus = BookStatus.fromValue(detail.userBookStatus) ?: BookStatus.BEFORE_READING
106+
selectedBookStatus = currentBookStatus
107+
seedsStates = seeds.categories.toImmutableList()
108+
readingRecords = records.readingRecords.toPersistentList()
109+
readingRecordsTotalCount = records.totalResults
104110

105-
isLastPage = records.lastPage
106-
currentStartIndex = START_INDEX
111+
isLastPage = records.lastPage
112+
currentStartIndex = START_INDEX
107113

108-
uiState = UiState.Success
109-
}
110-
} catch (e: Throwable) {
111-
uiState = UiState.Error(e)
114+
uiState = UiState.Success
115+
}
116+
} catch (e: Throwable) {
117+
uiState = UiState.Error(e)
112118

113-
val handleErrorMessage = { message: String ->
114-
Logger.e(message)
115-
sideEffect = BookDetailSideEffect.ShowToast(message)
116-
}
119+
val handleErrorMessage = { message: String ->
120+
Logger.e(message)
121+
sideEffect = BookDetailSideEffect.ShowToast(message)
122+
}
117123

118-
handleException(
119-
exception = e,
120-
onError = handleErrorMessage,
121-
onLoginRequired = {
122-
navigator.resetRoot(LoginScreen)
123-
},
124-
)
124+
handleException(
125+
exception = e,
126+
onError = handleErrorMessage,
127+
onLoginRequired = {
128+
navigator.resetRoot(LoginScreen)
129+
},
130+
)
131+
}
125132
}
126133
}
127134

@@ -182,6 +189,29 @@ class BookDetailPresenter @AssistedInject constructor(
182189
}
183190
}
184191

192+
fun deleteRecord(readingRecordId: String, onSuccess: () -> Unit) {
193+
scope.launch {
194+
recordRepository.deleteRecord(readingRecordId = readingRecordId)
195+
.onSuccess {
196+
onSuccess()
197+
}
198+
.onFailure { exception ->
199+
val handleErrorMessage = { message: String ->
200+
Logger.e(message)
201+
sideEffect = BookDetailSideEffect.ShowToast(message)
202+
}
203+
204+
handleException(
205+
exception = exception,
206+
onError = handleErrorMessage,
207+
onLoginRequired = {
208+
navigator.resetRoot(LoginScreen)
209+
},
210+
)
211+
}
212+
}
213+
}
214+
185215
LaunchedEffect(Unit) {
186216
initialLoad()
187217
}
@@ -230,6 +260,55 @@ class BookDetailPresenter @AssistedInject constructor(
230260
isRecordSortBottomSheetVisible = false
231261
}
232262

263+
is BookDetailUiEvent.OnRecordMenuClick -> {
264+
selectedRecordInfo = event.selectedRecordInfo
265+
isRecordMenuBottomSheetVisible = true
266+
}
267+
268+
is BookDetailUiEvent.OnRecordMenuBottomSheetDismiss -> {
269+
isRecordMenuBottomSheetVisible = false
270+
}
271+
272+
is BookDetailUiEvent.OnRecordDeleteDialogDismiss -> {
273+
isRecordDeleteDialogVisible = false
274+
}
275+
276+
is BookDetailUiEvent.OnEditRecordClick -> {
277+
isRecordMenuBottomSheetVisible = false
278+
navigator.goTo(
279+
RecordEditScreen(
280+
RecordEditArgs(
281+
id = selectedRecordInfo.id,
282+
pageNumber = selectedRecordInfo.pageNumber,
283+
quote = selectedRecordInfo.quote,
284+
review = selectedRecordInfo.review,
285+
emotionTags = selectedRecordInfo.emotionTags,
286+
bookTitle = selectedRecordInfo.bookTitle,
287+
bookPublisher = selectedRecordInfo.bookPublisher,
288+
bookCoverImageUrl = selectedRecordInfo.bookCoverImageUrl,
289+
author = selectedRecordInfo.author,
290+
),
291+
),
292+
)
293+
}
294+
295+
is BookDetailUiEvent.OnDeleteRecordClick -> {
296+
isRecordMenuBottomSheetVisible = false
297+
isRecordDeleteDialogVisible = true
298+
}
299+
300+
is BookDetailUiEvent.OnDelete -> {
301+
isRecordDeleteDialogVisible = false
302+
deleteRecord(
303+
readingRecordId = selectedRecordInfo.id,
304+
onSuccess = {
305+
readingRecords = readingRecords
306+
.filterNot { it.id == selectedRecordInfo.id }
307+
.toPersistentList()
308+
},
309+
)
310+
}
311+
233312
is BookDetailUiEvent.OnRecordItemClick -> {
234313
navigator.goTo(RecordDetailScreen(event.recordId))
235314
}
@@ -260,6 +339,9 @@ class BookDetailPresenter @AssistedInject constructor(
260339
currentBookStatus = currentBookStatus,
261340
selectedBookStatus = selectedBookStatus,
262341
currentRecordSort = currentRecordSort,
342+
selectedRecordInfo = selectedRecordInfo,
343+
isRecordMenuBottomSheetVisible = isRecordMenuBottomSheetVisible,
344+
isRecordDeleteDialogVisible = isRecordDeleteDialogVisible,
263345
sideEffect = sideEffect,
264346
eventSink = ::handleEvent,
265347
)

0 commit comments

Comments
 (0)