Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
3a06361
[BOOK-154] feat: apis - 내서재 동적검색 Controller 작성 (#48)
minwoo1999 Jul 20, 2025
bd130c0
[BOOK-154] feat: apis - 내서재 동적검색 dto 작성 (#48)
minwoo1999 Jul 20, 2025
a08c0f7
[BOOK-154] feat: apis - 내서재 동적검색 service 작성 (#48)
minwoo1999 Jul 20, 2025
21daa1d
[BOOK-154] feat: apis - 내서재 동적검색 usecase 작성 (#48)
minwoo1999 Jul 20, 2025
7a51a7f
[BOOK-154] feat: apis,buildSrc - 내서재 동적검색 querydsl config setting (#48)
minwoo1999 Jul 20, 2025
74430fe
[BOOK-154] refactor: domain,infra - JpaAuditing으로 날짜 설정 (#48)
minwoo1999 Jul 20, 2025
18d80d8
[BOOK-154] feat: domain- 내서재 동적검색을 위한 domainservice, vo 설정 (#48)
minwoo1999 Jul 20, 2025
9a4433a
[BOOK-154] feat: domain- 내서재 동적검색 시 괄호 제거하는 validator 분리 (#48)
minwoo1999 Jul 20, 2025
43fae80
[BOOK-154] feat: infra- 내서재 동적검색 시 querydsl config 설정 (#48)
minwoo1999 Jul 20, 2025
ee7d032
[BOOK-154] feat: infra- querydslrepository 구현 (#48)
minwoo1999 Jul 20, 2025
1fb5894
[BOOK-154] refactor: Update QueryDSL build directory path (#48)
minwoo1999 Jul 21, 2025
ff6ad35
[BOOK-154] refactor: domain - 도서 상태별 카운트 동적 계산
minwoo1999 Jul 21, 2025
cef9e45
[BOOK-154] refactor: infra - QueryDSL 쿼리 로직 개선 및 가독성 향상 (#48)
minwoo1999 Jul 21, 2025
747ef27
refactor: apis,infra - pageconfig 분리
Jul 22, 2025
1a143d1
refactor: apis - 코드리뷰 1차 반영 (#48)
Jul 22, 2025
17b7df1
refactor: infra - querydsl refactoring deprecated code (#48)
Jul 22, 2025
a482901
refactor: infra - pageconfig 분리(#48)
Jul 22, 2025
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
2 changes: 1 addition & 1 deletion apis/src/main/kotlin/org/yapp/apis/ApisApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import org.springframework.boot.runApplication
"org.yapp.domain",
"org.yapp.gateway",
"org.yapp.globalutils"
] ,
],
exclude = [JpaRepositoriesAutoConfiguration::class]
)
class ApisApplication
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
package org.yapp.apis.book.controller

import jakarta.validation.Valid
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Sort
import org.springframework.data.web.PageableDefault
import org.springframework.http.ResponseEntity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.ModelAttribute
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import org.yapp.apis.book.dto.request.BookDetailRequest
import org.yapp.apis.book.dto.request.BookSearchRequest
import org.yapp.apis.book.dto.request.UserBookRegisterRequest
import org.yapp.apis.book.dto.response.BookDetailResponse
import org.yapp.apis.book.dto.response.BookSearchResponse
import org.yapp.apis.book.dto.response.UserBookPageResponse
import org.yapp.apis.book.dto.response.UserBookResponse
import org.yapp.apis.book.usecase.BookUseCase
import org.yapp.domain.userbook.BookStatus
import java.util.UUID

@RestController
Expand Down Expand Up @@ -53,10 +60,14 @@ class BookController(

@GetMapping("/my-library")
override fun getUserLibraryBooks(
@AuthenticationPrincipal userId: UUID
): ResponseEntity<List<UserBookResponse>> {

val response = bookUseCase.getUserLibraryBooks(userId)
@AuthenticationPrincipal userId: UUID,
@RequestParam(required = false) status: BookStatus?,
@RequestParam(required = false) sort: String?,
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

정렬 파라미터에 대한 검증 추가를 고려해보세요.

sort 파라미터가 String 타입으로 받아지고 있는데, 허용되는 정렬 필드에 대한 검증을 추가하는 것이 좋겠습니다. 이를 통해 잘못된 필드명으로 인한 오류를 방지할 수 있습니다.

다음과 같은 검증 로직을 추가할 수 있습니다:

@RequestParam(required = false) sort: String?,

@RequestParam(required = false) 
@Pattern(regexp = "^(createdAt|updatedAt|title)$", message = "정렬 필드는 createdAt, updatedAt, title 중 하나여야 합니다")
sort: String?,

로 변경하거나, enum을 사용하는 방법도 고려해볼 수 있습니다.

🤖 Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/book/controller/BookController.kt at line
65, the sort parameter lacks validation for allowed values. Add validation by
annotating the sort parameter with a pattern constraint that restricts input to
"createdAt", "updatedAt", or "title". Alternatively, replace the String type
with an enum representing these allowed fields to enforce validation at the type
level.

@PageableDefault(size = 10, sort = ["createdAt"], direction = Sort.Direction.DESC)
pageable: Pageable
): ResponseEntity<UserBookPageResponse> {
val response = bookUseCase.getUserLibraryBooks(userId, status, sort, pageable)
return ResponseEntity.ok(response)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,25 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.validation.Valid
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Sort
import org.springframework.data.web.PageableDefault
import org.springframework.http.ResponseEntity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.yapp.apis.book.dto.request.BookDetailRequest
import org.yapp.apis.book.dto.request.BookSearchRequest
import org.yapp.apis.book.dto.request.UserBookRegisterRequest
import org.yapp.apis.book.dto.response.BookDetailResponse
import org.yapp.apis.book.dto.response.BookSearchResponse
import org.yapp.apis.book.dto.response.UserBookPageResponse
import org.yapp.apis.book.dto.response.UserBookResponse
import org.yapp.domain.userbook.BookStatus
import org.yapp.globalutils.exception.ErrorResponse
import java.util.UUID

Expand Down Expand Up @@ -130,5 +137,12 @@ interface BookControllerApi {
]
)
@GetMapping("/my-library")
fun getUserLibraryBooks(@AuthenticationPrincipal userId: UUID): ResponseEntity<List<UserBookResponse>>
fun getUserLibraryBooks(
@AuthenticationPrincipal userId: UUID,
@RequestParam(required = false) status: BookStatus?,
@RequestParam(required = false) sort: String?,
@PageableDefault(size = 10, sort = ["createdAt"], direction = Sort.Direction.DESC)
pageable: Pageable
): ResponseEntity<UserBookPageResponse>
Comment on lines +140 to +146
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

동적 검색 API 인터페이스 개선 승인

사용자 서재 조회 API가 필터링, 정렬, 페이징을 지원하도록 확장되었습니다. 파라미터 설계와 기본값 설정이 적절합니다.

기본 페이지 크기(10)와 정렬 기준(createdAt DESC)이 사용자 경험에 적합한지 검토해보세요.

🤖 Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/book/controller/BookControllerApi.kt
around lines 140 to 146, the getUserLibraryBooks function has been updated to
support filtering, sorting, and paging with default page size 10 and sorting by
createdAt descending. Review and confirm that the default page size of 10 and
sorting by createdAt DESC are appropriate for the user experience; if needed,
adjust the @PageableDefault annotation parameters to better fit expected usage.


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.yapp.apis.book.dto.response

import io.swagger.v3.oas.annotations.media.Schema
import org.springframework.data.domain.Page

@Schema(description = "사용자의 책 페이지 응답")
data class UserBookPageResponse private constructor(

@Schema(description = "책 목록 (페이지네이션)", implementation = UserBookResponse::class)
val books: Page<UserBookResponse>,

@Schema(description = "읽기 전 상태의 책 개수")
val beforeReadingCount: Long,

@Schema(description = "읽고 있는 책 개수")
val readingCount: Long,

@Schema(description = "완독한 책 개수")
val completedCount: Long
) {
companion object {
fun of(
books: Page<UserBookResponse>,
beforeReadingCount: Long,
readingCount: Long,
completedCount: Long
): UserBookPageResponse {
return UserBookPageResponse(
books = books,
beforeReadingCount = beforeReadingCount,
readingCount = readingCount,
completedCount = completedCount
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.yapp.apis.book.dto.response

import org.yapp.domain.userbook.BookStatus
import org.yapp.domain.userbook.vo.UserBookInfoVO
import org.yapp.globalutils.validator.BookDataValidator
import java.time.format.DateTimeFormatter
import java.util.UUID

Expand All @@ -26,10 +27,10 @@ data class UserBookResponse private constructor(
userId = userBook.userId.value,
bookIsbn = userBook.bookIsbn.value,
bookTitle = userBook.title,
bookAuthor = userBook.author,
bookAuthor = BookDataValidator.removeParenthesesFromAuthor(userBook.author),
status = userBook.status,
coverImageUrl = userBook.coverImageUrl,
publisher = userBook.publisher,
publisher = BookDataValidator.removeParenthesesFromPublisher(userBook.publisher),
createdAt = userBook.createdAt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
updatedAt = userBook.updatedAt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class BookManagementService(
request.validIsbn(),
request.validTitle(),
request.validAuthor(),
request.validAuthor(),
request.validPublisher(),
Copy link
Member

Choose a reason for hiding this comment

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

굿굿!

request.coverImageUrl,
request.publicationYear,
request.description
Expand Down
36 changes: 33 additions & 3 deletions apis/src/main/kotlin/org/yapp/apis/book/service/UserBookService.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package org.yapp.apis.book.service

import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Service
import org.yapp.apis.auth.dto.request.UserBooksByIsbnsRequest
import org.yapp.apis.book.dto.response.UserBookPageResponse
import org.yapp.apis.book.dto.response.UserBookResponse
import org.yapp.apis.book.dto.request.UpsertUserBookRequest
import org.yapp.domain.userbook.BookStatus
import org.yapp.domain.userbook.UserBookDomainService
import org.yapp.domain.userbook.vo.UserBookInfoVO
import org.yapp.domain.userbook.vo.UserBookStatusCountsVO
import java.util.UUID


Expand All @@ -18,9 +23,9 @@ class UserBookService(
userBookDomainService.upsertUserBook(
upsertUserBookRequest.userId,
upsertUserBookRequest.bookIsbn,
upsertUserBookRequest.bookPublisher,
upsertUserBookRequest.bookAuthor,
upsertUserBookRequest.bookTitle,
upsertUserBookRequest.bookAuthor,
upsertUserBookRequest.bookPublisher,
upsertUserBookRequest.bookCoverImageUrl,
upsertUserBookRequest.status
)
Expand All @@ -42,5 +47,30 @@ class UserBookService(
.map { UserBookResponse.from(it) }
}

}
private fun findUserBooksByDynamicCondition(
userId: UUID,
status: BookStatus?,
sort: String?,
pageable: Pageable
): Page<UserBookResponse> {
return userBookDomainService.findUserBooksByDynamicCondition(userId, status, sort, pageable)
.map { UserBookResponse.from(it) }
}

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

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

변수명을 더 명확하게 개선할 수 있습니다.

과거 리뷰 의견을 반영하여 VO임을 명시적으로 드러내는 변수명을 사용하면 좋겠습니다.

다음과 같이 변수명을 개선해보세요:

-        val userBookStatusCountsVO = userBookDomainService.getUserBookStatusCounts(userId)
+        val statusCountsVO = userBookDomainService.getUserBookStatusCounts(userId)

또는 VO임을 더 명확히 표현하려면:

-        val userBookStatusCountsVO = userBookDomainService.getUserBookStatusCounts(userId)
+        val bookStatusCountsVO = userBookDomainService.getUserBookStatusCounts(userId)
🤖 Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/book/service/UserBookService.kt at line
67, the variable name userBookStatusCountsVO should be improved to more
explicitly indicate that it is a Value Object (VO). Rename the variable to
include "VO" clearly in its name, such as userBookStatusCountsVO or
userBookStatusCountsValueObject, to enhance code readability and maintain
consistency with past review suggestions.


return UserBookPageResponse.of(
books = userBookResponsePage,
beforeReadingCount = userBookStatusCountsVO.beforeReadingCount,
readingCount = userBookStatusCountsVO.readingCount,
completedCount = userBookStatusCountsVO.completedCount
)
}
}
13 changes: 11 additions & 2 deletions apis/src/main/kotlin/org/yapp/apis/book/usecase/BookUseCase.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.yapp.apis.book.usecase

import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.transaction.annotation.Transactional
import org.yapp.apis.auth.dto.request.UserBooksByIsbnsRequest
import org.yapp.apis.auth.service.UserAuthService
Expand All @@ -10,12 +12,14 @@ import org.yapp.apis.book.dto.request.BookSearchRequest
import org.yapp.apis.book.dto.request.UserBookRegisterRequest
import org.yapp.apis.book.dto.response.BookDetailResponse
import org.yapp.apis.book.dto.response.BookSearchResponse
import org.yapp.apis.book.dto.response.UserBookPageResponse
import org.yapp.apis.book.dto.response.UserBookResponse
import org.yapp.apis.book.constant.BookQueryServiceQualifier
import org.yapp.apis.book.dto.request.UpsertUserBookRequest
import org.yapp.apis.book.service.BookManagementService
import org.yapp.apis.book.service.BookQueryService
import org.yapp.apis.book.service.UserBookService
import org.yapp.domain.userbook.BookStatus
import org.yapp.globalutils.annotation.UseCase
import java.util.UUID

Expand Down Expand Up @@ -66,9 +70,14 @@ class BookUseCase(
return userBookResponse
}

fun getUserLibraryBooks(userId: UUID): List<UserBookResponse> {
fun getUserLibraryBooks(
userId: UUID,
status: BookStatus?,
sort: String?,
pageable: Pageable
): UserBookPageResponse {
userAuthService.validateUserExists(userId)

return userBookService.findAllUserBooks(userId)
return userBookService.findUserBooksByDynamicConditionWithStatusCounts(userId, status, sort, pageable)
}
Comment on lines +73 to 82
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

동적 검색 기능이 올바르게 구현되었습니다.

다음과 같은 좋은 점들이 있습니다:

  • 사용자 존재 검증 유지
  • 선택적 필터링과 정렬 파라미터 지원
  • 서비스 계층으로의 적절한 위임
  • 페이지네이션 응답 타입 사용

다만 sort 파라미터에 대한 유효성 검증을 고려해보세요. 허용되지 않는 정렬 필드가 전달될 경우를 대비한 검증 로직이 있으면 더 안전할 것 같습니다.

🤖 Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/book/usecase/BookUseCase.kt around lines
73 to 82, the getUserLibraryBooks function accepts a sort parameter without
validation. To fix this, add validation logic to check if the provided sort
value is among the allowed sorting fields before passing it to the service. If
the sort parameter is invalid, handle it appropriately by either defaulting to a
safe value or throwing a validation exception to ensure only valid sort criteria
are processed.

}
4 changes: 3 additions & 1 deletion apis/src/main/kotlin/org/yapp/apis/config/InfraConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import org.yapp.infra.InfraBaseConfigGroup
InfraBaseConfigGroup.JPA,
InfraBaseConfigGroup.ASYNC,
InfraBaseConfigGroup.REDIS,
InfraBaseConfigGroup.REST_CLIENT
InfraBaseConfigGroup.REST_CLIENT,
InfraBaseConfigGroup.QUERY_DSL,
InfraBaseConfigGroup.PAGE,
]
)
class InfraConfig
20 changes: 20 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,26 @@ subprojects {
}
}

// QueryDSL 설정
val querydslDir = "${layout.buildDirectory.get()}/generated/querydsl"


sourceSets {
main {
kotlin {
srcDir(querydslDir)
}
}
}


tasks.withType<org.jetbrains.kotlin.gradle.tasks.KaptGenerateStubs>().configureEach {
doFirst {
delete(querydslDir)
}
}
Comment on lines +91 to +95
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

KAPT 태스크 설정 확인

KAPT 실행 전 디렉토리 정리 로직은 적절하지만, 너무 공격적일 수 있습니다. 증분 빌드 최적화를 고려하여 필요한 경우에만 정리하는 것을 검토해보세요.

🤖 Prompt for AI Agents
In build.gradle.kts around lines 94 to 98, the current KAPT task configuration
deletes the querydslDir directory unconditionally before execution, which can be
too aggressive and harm incremental build performance. Modify the doFirst block
to check if the directory exists and contains files that require deletion, or
implement a condition to delete only when necessary, thus preserving incremental
build optimizations.



// 루트 프로젝트에서 모든 JaCoCo 설정 관리
configure(subprojects) {
jacoco {
Expand Down
8 changes: 7 additions & 1 deletion buildSrc/src/main/kotlin/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ object Dependencies {
const val BOOT_STARTER_ACTUATOR = "org.springframework.boot:spring-boot-starter-actuator"
const val BOOT_STARTER_TEST = "org.springframework.boot:spring-boot-starter-test"
const val BOOT_STARTER_DATA_REDIS = "org.springframework.boot:spring-boot-starter-data-redis"
const val BOOT_STARTER_OAUTH2_RESOURCE_SERVER = "org.springframework.boot:spring-boot-starter-oauth2-resource-server"
const val BOOT_STARTER_OAUTH2_RESOURCE_SERVER =
"org.springframework.boot:spring-boot-starter-oauth2-resource-server"
const val KOTLIN_REFLECT = "org.jetbrains.kotlin:kotlin-reflect"
}

Expand Down Expand Up @@ -51,4 +52,9 @@ object Dependencies {
object Flyway {
const val MYSQL = "org.flywaydb:flyway-mysql"
}

object QueryDsl {
const val JPA = "com.querydsl:querydsl-jpa:5.0.0:jakarta"
const val APT = "com.querydsl:querydsl-apt:5.0.0:jakarta"
}
}
12 changes: 4 additions & 8 deletions domain/src/main/kotlin/org/yapp/domain/book/Book.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ data class Book private constructor(
val publicationYear: Int?,
val coverImageUrl: String,
val description: String?,
val createdAt: LocalDateTime,
val updatedAt: LocalDateTime,
val createdAt: LocalDateTime? = null,
val updatedAt: LocalDateTime? = null,
val deletedAt: LocalDateTime? = null
) {
companion object {
Expand All @@ -28,7 +28,6 @@ data class Book private constructor(
publicationYear: Int? = null,
description: String? = null
): Book {
val now = LocalDateTime.now()
return Book(
isbn = Isbn.newInstance(isbn),
title = title,
Expand All @@ -37,9 +36,6 @@ data class Book private constructor(
publicationYear = publicationYear,
coverImageUrl = coverImageUrl,
description = description,
createdAt = now,
updatedAt = now,
deletedAt = null
)
}

Expand All @@ -51,8 +47,8 @@ data class Book private constructor(
publicationYear: Int?,
coverImageUrl: String,
description: String?,
createdAt: LocalDateTime,
updatedAt: LocalDateTime,
createdAt: LocalDateTime? = null,
updatedAt: LocalDateTime? = null,
deletedAt: LocalDateTime? = null
): Book {
return Book(
Expand Down
Loading
Loading