Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,11 @@ class BookController(
@AuthenticationPrincipal userId: UUID,
@RequestParam(required = false) status: BookStatus?,
@RequestParam(required = false) sort: UserBookSortType?,
@RequestParam(required = false) title: String?,
@PageableDefault(size = 10, sort = ["createdAt"], direction = Sort.Direction.DESC)
pageable: Pageable
): ResponseEntity<UserBookPageResponse> {
val response = bookUseCase.getUserLibraryBooks(userId, status, sort, pageable)
val response = bookUseCase.getUserLibraryBooks(userId, status, sort, title, pageable)
return ResponseEntity.ok(response)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ interface BookControllerApi {
@Valid @RequestBody request: UserBookRegisterRequest
): ResponseEntity<UserBookResponse>

@Operation(summary = "사용자 서재 조회", description = "현재 사용자의 서재에 등록된 모든 책을 조회합니다.")
@Operation(summary = "사용자 서재 조회", description = "현재 사용자의 서재에 등록된 모든 책을 조회합니다. 제목(title)으로 검색할 수 있습니다.")
@ApiResponses(
value = [
ApiResponse(
Expand All @@ -140,8 +140,9 @@ interface BookControllerApi {
@GetMapping("/my-library")
fun getUserLibraryBooks(
@AuthenticationPrincipal userId: UUID,
@RequestParam(required = false) status: BookStatus?,
@RequestParam(required = false) sort: UserBookSortType?,
@RequestParam(required = false) @Parameter(description = "책 상태 필터") status: BookStatus?,
@RequestParam(required = false) @Parameter(description = "정렬 방식") sort: UserBookSortType?,
@RequestParam(required = false) @Parameter(description = "책 제목 검색") title: String?,
@PageableDefault(size = 10, sort = ["createdAt"], direction = Sort.Direction.DESC)
pageable: Pageable
): ResponseEntity<UserBookPageResponse>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,21 @@ class UserBookService(
userId: UUID,
status: BookStatus?,
sort: UserBookSortType?,
title: String?,
pageable: Pageable
): Page<UserBookResponse> {
val page = userBookDomainService.findUserBooksByDynamicCondition(userId, status, sort, pageable)
val page = userBookDomainService.findUserBooksByDynamicCondition(userId, status, sort, title, pageable)
return page.map { UserBookResponse.from(it) }
}

fun findUserBooksByDynamicConditionWithStatusCounts(
userId: UUID,
status: BookStatus?,
sort: UserBookSortType?,
title: String?,
pageable: Pageable
): UserBookPageResponse {
val userBookResponsePage = findUserBooksByDynamicCondition(userId, status, sort, pageable)
val userBookResponsePage = findUserBooksByDynamicCondition(userId, status, sort, title, pageable)
val userBookStatusCountsVO = userBookDomainService.getUserBookStatusCounts(userId)

return UserBookPageResponse.of(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,11 @@ class BookUseCase(
userId: UUID,
status: BookStatus?,
sort: UserBookSortType?,
title: String?,
pageable: Pageable
): UserBookPageResponse {
userAuthService.validateUserExists(userId)

return userBookService.findUserBooksByDynamicConditionWithStatusCounts(userId, status, sort, pageable)
return userBookService.findUserBooksByDynamicConditionWithStatusCounts(userId, status, sort, title, pageable)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ interface ReadingRecordControllerApi {
ApiResponse(
responseCode = "200",
description = "독서 기록 목록 조회 성공",
content = [Content(schema = Schema(implementation = ReadingRecordResponse::class))]
content = [Content(schema = Schema(implementation = Page::class))]
),
ApiResponse(
responseCode = "404",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ class UserBookDomainService(
userId: UUID,
status: BookStatus?,
sort: UserBookSortType?,
title: String?,
pageable: Pageable
): Page<UserBookInfoVO> {
val page = userBookRepository.findUserBooksByDynamicCondition(userId, status, sort, pageable)
val page = userBookRepository.findUserBooksByDynamicCondition(userId, status, sort, title, pageable)
return page.map { UserBookInfoVO.newInstance(it) }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface UserBookRepository {
userId: UUID,
status: BookStatus?,
sort: UserBookSortType?,
title: String?,
pageable: Pageable
): Page<UserBook>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@ import jakarta.persistence.*
import org.hibernate.annotations.JdbcTypeCode
import org.hibernate.annotations.SQLDelete
import org.hibernate.annotations.SQLRestriction
import jakarta.persistence.Index
import org.yapp.infra.common.BaseTimeEntity
import org.yapp.domain.userbook.BookStatus
import org.yapp.domain.userbook.UserBook
import java.sql.Types
import java.util.*

@Entity
@Table(name = "user_books")
@Table(
name = "user_books",
indexes = [
Index(name = "idx_user_books_title", columnList = "title"),
Index(name = "idx_user_books_user_id_title", columnList = "user_id, title")
]
)
@SQLDelete(sql = "UPDATE user_books SET deleted_at = NOW() WHERE id = ?")
@SQLRestriction("deleted_at IS NULL")
class UserBookEntity(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface JpaUserBookQuerydslRepository {
userId: UUID,
status: BookStatus?,
sort: UserBookSortType?,
title: String?,
pageable: Pageable
): Page<UserBookEntity>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ class JpaUserBookQuerydslRepositoryImpl(
userId: UUID,
status: BookStatus?,
sort: UserBookSortType?,
title: String?,
pageable: Pageable
): Page<UserBookEntity> {
val baseQuery = queryFactory
.selectFrom(userBook)
.where(
userBook.userId.eq(userId),
statusEq(status)
statusEq(status),
titleContains(title)
)

val results = baseQuery
Expand All @@ -45,7 +47,8 @@ class JpaUserBookQuerydslRepositoryImpl(
.from(userBook)
.where(
userBook.userId.eq(userId),
statusEq(status)
statusEq(status),
titleContains(title)
)
.fetchOne() ?: 0L

Expand All @@ -70,6 +73,12 @@ class JpaUserBookQuerydslRepositoryImpl(
return status?.let { userBook.status.eq(it) }
}

private fun titleContains(title: String?): BooleanExpression? {
return title?.takeIf { it.isNotBlank() }?.let {
userBook.title.like("%" + it + "%")
}
}
Comment on lines +76 to +80
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

LIKE 검색 구현이 올바르지만 성능 고려사항이 있습니다.

titleContains 메서드가 적절하게 구현되었고 빈 문자열 처리도 잘 되어 있습니다. 다만 LIKE '%...%' 패턴은 인덱스를 완전히 활용하지 못할 수 있습니다. PR 목표에서 언급한 것처럼 향후 Full-Text Search 구현을 고려해보시기 바랍니다.

 private fun titleContains(title: String?): BooleanExpression? {
     return title?.takeIf { it.isNotBlank() }?.let {
-        userBook.title.like("%" + it + "%")
+        userBook.title.like("%" + it.replace("%", "\\%").replace("_", "\\_") + "%")
     }
 }

추가적으로 특수문자 이스케이핑을 고려해보세요.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private fun titleContains(title: String?): BooleanExpression? {
return title?.takeIf { it.isNotBlank() }?.let {
userBook.title.like("%" + it + "%")
}
}
private fun titleContains(title: String?): BooleanExpression? {
return title?.takeIf { it.isNotBlank() }?.let {
userBook.title.like(
"%" + it.replace("%", "\\%")
.replace("_", "\\_") + "%"
)
}
}
🤖 Prompt for AI Agents
In
infra/src/main/kotlin/org/yapp/infra/userbook/repository/impl/JpaUserBookQuerydslRepositoryImpl.kt
around lines 76 to 80, the titleContains method uses a LIKE '%...%' pattern
which can degrade performance and does not handle special characters properly.
To fix this, implement escaping for special characters in the input title string
before constructing the LIKE pattern, and consider planning for a future
migration to Full-Text Search for better performance and accuracy.


private fun createOrderSpecifier(sort: UserBookSortType?): OrderSpecifier<*> {
return when (sort) {
UserBookSortType.TITLE_ASC -> userBook.title.asc()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,10 @@ class UserBookRepositoryImpl(
userId: UUID,
status: BookStatus?,
sort: UserBookSortType?,
title: String?,
pageable: Pageable
): Page<UserBook> {
return jpaUserBookRepository.findUserBooksByDynamicCondition(userId, status, sort, pageable)
return jpaUserBookRepository.findUserBooksByDynamicCondition(userId, status, sort, title, pageable)
.map { it.toDomain() }
}

Expand Down