Skip to content

Commit c0bbc2e

Browse files
authored
feat: 기록 수정, 삭제 기능 추가 (#94)
* [BOOK-262] feat: apis - 독서 기록 수정,삭제 기능구현 (#93) * [BOOK-262] feat: �domain - 독서 기록 수정,삭제 기능구현 (#93) * [BOOK-262] feat: �infra - 독서 기록 수정,삭제 기능구현 (#93) * [BOOK-262] fix: apis - 독서기록 DTO, service 수정 (#94) * [BOOK-262] fix: apis,gateway - 도서검색 로그인 없이 사용할 수 있도록 수정(#94) * [BOOK-262] refacator: apis,domain - 코드리뷰 반영 (#94) * [BOOK-262] �refactor: dto validtation 처리 (#94)
1 parent bbeb399 commit c0bbc2e

File tree

18 files changed

+269
-27
lines changed

18 files changed

+269
-27
lines changed

apis/src/main/kotlin/org/yapp/apis/book/controller/BookController.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,9 @@ class BookController(
3333

3434
@GetMapping("/search")
3535
override fun searchBooks(
36-
@AuthenticationPrincipal userId: UUID,
3736
@Valid @ModelAttribute request: BookSearchRequest
3837
): ResponseEntity<BookSearchResponse> {
39-
val response = bookUseCase.searchBooks(request, userId)
38+
val response = bookUseCase.searchBooks(request)
4039
return ResponseEntity.ok(response)
4140
}
4241

apis/src/main/kotlin/org/yapp/apis/book/controller/BookControllerApi.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ interface BookControllerApi {
5151
)
5252
@GetMapping("/search")
5353
fun searchBooks(
54-
@AuthenticationPrincipal userId: UUID,
5554
@Valid @Parameter(description = "도서 검색 요청 객체") request: BookSearchRequest
5655
): ResponseEntity<BookSearchResponse>
5756

apis/src/main/kotlin/org/yapp/apis/book/usecase/BookUseCase.kt

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,10 @@ class BookUseCase(
2929
private val bookManagementService: BookManagementService
3030
) {
3131
fun searchBooks(
32-
request: BookSearchRequest,
33-
userId: UUID
32+
request: BookSearchRequest
3433
): BookSearchResponse {
35-
userService.validateUserExists(userId)
36-
3734
val searchResponse = bookQueryService.searchBooks(request)
38-
val booksWithUserStatus = mergeWithUserBookStatus(searchResponse.books, userId)
35+
val booksWithUserStatus = mergeWithUserBookStatus(searchResponse.books, null)
3936

4037
return searchResponse.withUpdatedBooks(booksWithUserStatus)
4138
}
@@ -89,10 +86,10 @@ class BookUseCase(
8986

9087
private fun mergeWithUserBookStatus(
9188
searchedBooks: List<BookSummary>,
92-
userId: UUID
89+
userId: UUID?
9390
): List<BookSummary> {
94-
if (searchedBooks.isEmpty()) {
95-
return emptyList()
91+
if (userId == null || searchedBooks.isEmpty()) {
92+
return searchedBooks
9693
}
9794

9895
val isbn13s = searchedBooks.map { it.isbn13 }
@@ -107,8 +104,9 @@ class BookUseCase(
107104

108105
private fun getUserBookStatusMap(
109106
isbn13s: List<String>,
110-
userId: UUID
107+
userId: UUID?
111108
): Map<String, BookStatus> {
109+
if (userId == null) return emptyMap()
112110
val userBooksResponse = userBookService.findAllByUserIdAndBookIsbn13In(
113111
UserBooksByIsbn13sRequest.of(userId, isbn13s)
114112
)

apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordController.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@ import org.springframework.data.web.PageableDefault
77
import org.springframework.http.HttpStatus
88
import org.springframework.http.ResponseEntity
99
import org.springframework.security.core.annotation.AuthenticationPrincipal
10+
import org.springframework.web.bind.annotation.DeleteMapping
1011
import org.springframework.web.bind.annotation.GetMapping
1112
import org.springframework.web.bind.annotation.PathVariable
1213
import org.springframework.web.bind.annotation.PostMapping
14+
import org.springframework.web.bind.annotation.PatchMapping
1315
import org.springframework.web.bind.annotation.RequestBody
1416
import org.springframework.web.bind.annotation.RequestMapping
1517
import org.springframework.web.bind.annotation.RequestParam
1618
import org.springframework.web.bind.annotation.RestController
1719
import org.yapp.apis.readingrecord.dto.request.CreateReadingRecordRequest
20+
import org.yapp.apis.readingrecord.dto.request.UpdateReadingRecordRequest
1821
import org.yapp.apis.readingrecord.dto.response.ReadingRecordPageResponse // Added import
1922
import org.yapp.apis.readingrecord.dto.response.ReadingRecordResponse
2023
import org.yapp.apis.readingrecord.usecase.ReadingRecordUseCase
@@ -80,4 +83,27 @@ class ReadingRecordController(
8083
val stats = readingRecordUseCase.getSeedStats(userId, userBookId)
8184
return ResponseEntity.ok(stats)
8285
}
86+
87+
@PatchMapping("/{readingRecordId}")
88+
override fun updateReadingRecord(
89+
@AuthenticationPrincipal userId: UUID,
90+
@PathVariable readingRecordId: UUID,
91+
@Valid @RequestBody request: UpdateReadingRecordRequest
92+
): ResponseEntity<ReadingRecordResponse> {
93+
val response = readingRecordUseCase.updateReadingRecord(
94+
userId = userId,
95+
readingRecordId = readingRecordId,
96+
request = request
97+
)
98+
return ResponseEntity.ok(response)
99+
}
100+
101+
@DeleteMapping("/{readingRecordId}")
102+
override fun deleteReadingRecord(
103+
@AuthenticationPrincipal userId: UUID,
104+
@PathVariable readingRecordId: UUID
105+
): ResponseEntity<Unit> {
106+
readingRecordUseCase.deleteReadingRecord(userId, readingRecordId)
107+
return ResponseEntity.noContent().build()
108+
}
83109
}

apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerApi.kt

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import org.springframework.http.ResponseEntity
1616
import org.springframework.security.core.annotation.AuthenticationPrincipal
1717
import org.springframework.web.bind.annotation.*
1818
import org.yapp.apis.readingrecord.dto.request.CreateReadingRecordRequest
19+
import org.yapp.apis.readingrecord.dto.request.UpdateReadingRecordRequest
1920
import org.yapp.apis.readingrecord.dto.response.ReadingRecordPageResponse
2021
import org.yapp.apis.readingrecord.dto.response.ReadingRecordResponse
2122
import org.yapp.apis.readingrecord.dto.response.SeedStatsResponse
@@ -131,4 +132,58 @@ interface ReadingRecordControllerApi {
131132
@AuthenticationPrincipal userId: UUID,
132133
@PathVariable userBookId: UUID
133134
): ResponseEntity<SeedStatsResponse>
135+
136+
@Operation(
137+
summary = "독서 기록 수정",
138+
description = "독서 기록 ID로 독서 기록을 수정합니다."
139+
)
140+
@ApiResponses(
141+
value = [
142+
ApiResponse(
143+
responseCode = "200",
144+
description = "독서 기록 수정 성공",
145+
content = [Content(schema = Schema(implementation = ReadingRecordResponse::class))]
146+
),
147+
ApiResponse(
148+
responseCode = "400",
149+
description = "잘못된 요청",
150+
content = [Content(schema = Schema(implementation = ErrorResponse::class))]
151+
),
152+
153+
ApiResponse(
154+
responseCode = "404",
155+
description = "독서 기록을 찾을 수 없음",
156+
content = [Content(schema = Schema(implementation = ErrorResponse::class))]
157+
)
158+
]
159+
)
160+
@PatchMapping("/{readingRecordId}")
161+
fun updateReadingRecord(
162+
@AuthenticationPrincipal @Parameter(description = "인증된 사용자 ID") userId: UUID,
163+
@PathVariable @Parameter(description = "수정할 독서 기록 ID") readingRecordId: UUID,
164+
@Valid @RequestBody @Parameter(description = "독서 기록 수정 요청 객체") request: UpdateReadingRecordRequest
165+
): ResponseEntity<ReadingRecordResponse>
166+
167+
@Operation(
168+
summary = "독서 기록 삭제",
169+
description = "독서 기록 ID로 독서 기록을 삭제합니다."
170+
)
171+
@ApiResponses(
172+
value = [
173+
ApiResponse(
174+
responseCode = "204",
175+
description = "독서 기록 삭제 성공"
176+
),
177+
ApiResponse(
178+
responseCode = "404",
179+
description = "독서 기록을 찾을 수 없음",
180+
content = [Content(schema = Schema(implementation = ErrorResponse::class))]
181+
)
182+
]
183+
)
184+
@DeleteMapping("/{readingRecordId}")
185+
fun deleteReadingRecord(
186+
@AuthenticationPrincipal @Parameter(description = "인증된 사용자 ID") userId: UUID,
187+
@PathVariable @Parameter(description = "삭제할 독서 기록 ID") readingRecordId: UUID
188+
): ResponseEntity<Unit>
134189
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package org.yapp.apis.readingrecord.dto.request
2+
3+
import io.swagger.v3.oas.annotations.media.Schema
4+
import jakarta.validation.constraints.Max
5+
import jakarta.validation.constraints.Min
6+
import jakarta.validation.constraints.Size
7+
8+
@Schema(
9+
name = "UpdateReadingRecordRequest",
10+
description = "독서 기록 수정 요청",
11+
example = """
12+
{
13+
"pageNumber": 50,
14+
"quote": "수정된 기억에 남는 문장입니다.",
15+
"review": "수정된 감상평입니다.",
16+
"emotionTags": ["놀라움"]
17+
}
18+
"""
19+
)
20+
data class UpdateReadingRecordRequest private constructor(
21+
@field:Min(1, message = "페이지 번호는 1 이상이어야 합니다.")
22+
@field:Max(9999, message = "페이지 번호는 9999 이하여야 합니다.")
23+
@field:Schema(description = "수정할 페이지 번호", example = "50")
24+
val pageNumber: Int?,
25+
26+
@field:Size(max = 1000, message = "기억에 남는 문장은 1000자를 초과할 수 없습니다.")
27+
@field:Schema(description = "수정할 기억에 남는 문장", example = "수정된 기억에 남는 문장입니다.")
28+
val quote: String?,
29+
30+
@field:Size(max = 1000, message = "감상평은 1000자를 초과할 수 없습니다.")
31+
@field:Schema(description = "수정할 감상평", example = "수정된 감상평입니다.")
32+
val review: String?,
33+
34+
@field:Size(max = 3, message = "감정 태그는 최대 3개까지 가능합니다.")
35+
@field:Schema(description = "수정할 감정 태그 목록", example = """["따뜻함","즐거움","슬픔","깨달음"]""")
36+
val emotionTags: List<@Size(max = 10, message = "감정 태그는 10자를 초과할 수 없습니다.") String>?
37+
) {
38+
fun validPageNumber(): Int = pageNumber!!
39+
fun validQuote(): String = quote!!
40+
fun validReview(): String = review!!
41+
fun validEmotionTags(): List<String> = emotionTags!!
42+
}

apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordService.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package org.yapp.apis.readingrecord.service
33
import org.springframework.data.domain.Page
44
import org.springframework.data.domain.Pageable
55
import org.yapp.apis.readingrecord.dto.request.CreateReadingRecordRequest
6+
import org.yapp.apis.readingrecord.dto.request.UpdateReadingRecordRequest
67
import org.yapp.apis.readingrecord.dto.response.ReadingRecordResponse
78
import org.yapp.domain.readingrecord.ReadingRecordDomainService
89
import org.yapp.domain.readingrecord.ReadingRecordSortType
@@ -45,4 +46,24 @@ class ReadingRecordService(
4546
val page = readingRecordDomainService.findReadingRecordsByDynamicCondition(userBookId, sort, pageable)
4647
return page.map { ReadingRecordResponse.from(it) }
4748
}
49+
50+
fun updateReadingRecord(
51+
readingRecordId: UUID,
52+
request: UpdateReadingRecordRequest
53+
): ReadingRecordResponse {
54+
val readingRecordInfoVO = readingRecordDomainService.modifyReadingRecord(
55+
readingRecordId = readingRecordId,
56+
pageNumber = request.validPageNumber(),
57+
quote = request.validQuote(),
58+
review = request.validReview(),
59+
emotionTags = request.validEmotionTags()
60+
)
61+
return ReadingRecordResponse.from(readingRecordInfoVO)
62+
}
63+
64+
fun deleteReadingRecord(
65+
readingRecordId: UUID
66+
) {
67+
readingRecordDomainService.deleteReadingRecord(readingRecordId)
68+
}
4869
}

apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCase.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable
55
import org.springframework.transaction.annotation.Transactional
66
import org.yapp.apis.book.service.UserBookService
77
import org.yapp.apis.readingrecord.dto.request.CreateReadingRecordRequest
8+
import org.yapp.apis.readingrecord.dto.request.UpdateReadingRecordRequest
89
import org.yapp.apis.readingrecord.dto.response.ReadingRecordPageResponse // Added import
910
import org.yapp.apis.readingrecord.dto.response.ReadingRecordResponse
1011
import org.yapp.apis.readingrecord.dto.response.SeedStatsResponse
@@ -76,4 +77,27 @@ class ReadingRecordUseCase(
7677
userBookService.validateUserBookExists(userBookId, userId)
7778
return readingRecordTagService.getSeedStatsByUserIdAndUserBookId(userId, userBookId)
7879
}
80+
81+
@Transactional
82+
fun updateReadingRecord(
83+
userId: UUID,
84+
readingRecordId: UUID,
85+
request: UpdateReadingRecordRequest
86+
): ReadingRecordResponse {
87+
userService.validateUserExists(userId)
88+
89+
return readingRecordService.updateReadingRecord(
90+
readingRecordId = readingRecordId,
91+
request = request
92+
)
93+
}
94+
95+
@Transactional
96+
fun deleteReadingRecord(
97+
userId: UUID,
98+
readingRecordId: UUID
99+
) {
100+
userService.validateUserExists(userId)
101+
readingRecordService.deleteReadingRecord(readingRecordId)
102+
}
79103
}

domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecord.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,21 @@ data class ReadingRecord private constructor(
5858
}
5959
}
6060

61+
fun update(
62+
pageNumber: Int?,
63+
quote: String?,
64+
review: String?,
65+
emotionTags: List<String>?
66+
): ReadingRecord {
67+
return this.copy(
68+
pageNumber = pageNumber?.let { PageNumber.newInstance(it) } ?: this.pageNumber,
69+
quote = quote?.let { Quote.newInstance(it) } ?: this.quote,
70+
review = review?.let { Review.newInstance(it) } ?: this.review,
71+
emotionTags = emotionTags?.map { EmotionTag.newInstance(it) } ?: this.emotionTags,
72+
updatedAt = LocalDateTime.now()
73+
)
74+
}
75+
6176
@JvmInline
6277
value class Id(val value: UUID) {
6378
companion object {
@@ -114,4 +129,8 @@ data class ReadingRecord private constructor(
114129
}
115130
}
116131
}
132+
133+
fun delete(): ReadingRecord {
134+
return this.copy(deletedAt = LocalDateTime.now())
135+
}
117136
}

domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -102,24 +102,64 @@ class ReadingRecordDomainService(
102102
sort: ReadingRecordSortType?,
103103
pageable: Pageable
104104
): Page<ReadingRecordInfoVO> {
105-
val page = readingRecordRepository.findReadingRecordsByDynamicCondition(userBookId, sort, pageable)
106-
107-
// Get the UserBook entity to get the book thumbnail, title, and publisher
108-
val userBook = userBookRepository.findById(userBookId)
105+
return readingRecordRepository.findReadingRecordsByDynamicCondition(userBookId, sort, pageable)
106+
.map { buildReadingRecordInfoVO(it) }
107+
}
109108

110-
return page.map { readingRecord ->
111-
val readingRecordTags = readingRecordTagRepository.findByReadingRecordId(readingRecord.id.value)
112-
val tagIds = readingRecordTags.map { it.tagId.value }
113-
val tags = tagRepository.findByIds(tagIds)
114-
ReadingRecordInfoVO.newInstance(
115-
readingRecord = readingRecord,
116-
emotionTags = tags.map { it.name },
117-
bookTitle = userBook?.title,
118-
bookPublisher = userBook?.publisher,
119-
bookCoverImageUrl = userBook?.coverImageUrl,
120-
author = userBook?.author
109+
fun modifyReadingRecord(
110+
readingRecordId: UUID,
111+
pageNumber: Int?,
112+
quote: String?,
113+
review: String?,
114+
emotionTags: List<String>?
115+
): ReadingRecordInfoVO {
116+
val readingRecord = readingRecordRepository.findById(readingRecordId)
117+
?: throw ReadingRecordNotFoundException(
118+
ReadingRecordErrorCode.READING_RECORD_NOT_FOUND,
119+
"Reading record not found with id: $readingRecordId"
121120
)
121+
122+
val updatedReadingRecord = readingRecord.update(
123+
pageNumber = pageNumber,
124+
quote = quote,
125+
review = review,
126+
emotionTags = emotionTags
127+
)
128+
129+
val savedReadingRecord = readingRecordRepository.save(updatedReadingRecord)
130+
131+
// Update emotion tags
132+
if (emotionTags != null) {
133+
readingRecordTagRepository.deleteAllByReadingRecordId(readingRecordId)
134+
val tags = emotionTags.map { tagName ->
135+
tagRepository.findByName(tagName) ?: tagRepository.save(Tag.create(tagName))
136+
}
137+
val newReadingRecordTags = tags.map {
138+
ReadingRecordTag.create(
139+
readingRecordId = savedReadingRecord.id.value,
140+
tagId = it.id.value
141+
)
142+
}
143+
readingRecordTagRepository.saveAll(newReadingRecordTags)
122144
}
145+
146+
return buildReadingRecordInfoVO(savedReadingRecord)
123147
}
124148

149+
fun deleteReadingRecord(readingRecordId: UUID) {
150+
val readingRecord = readingRecordRepository.findById(readingRecordId)
151+
?: throw ReadingRecordNotFoundException(
152+
ReadingRecordErrorCode.READING_RECORD_NOT_FOUND,
153+
"Reading record not found with id: $readingRecordId"
154+
)
155+
156+
val userBook = userBookRepository.findById(readingRecord.userBookId.value)
157+
?: throw UserBookNotFoundException(
158+
UserBookErrorCode.USER_BOOK_NOT_FOUND,
159+
"User book not found with id: ${readingRecord.userBookId.value}"
160+
)
161+
162+
readingRecordRepository.deleteById(readingRecordId)
163+
userBookRepository.save(userBook.decreaseReadingRecordCount())
164+
}
125165
}

0 commit comments

Comments
 (0)