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 @@ -74,24 +74,28 @@ class BookQueryService(
fun filter(
level: Int?,
pageRange: String?,
origin: String?,
genre: String?,
origins: List<String>?,
genres: List<String>?,
keyword: String?,
cursor: Long?
): BookFilterResponse {
// 쉼표로 구분된 문자열도 지원
val parsedOrigins = origins?.flatMap { it.split(",") }?.map { it.trim() }?.filter { it.isNotBlank() }
val parsedGenres = genres?.flatMap { it.split(",") }?.map { it.trim() }?.filter { it.isNotBlank() }

// 필터 없이 검색어만 입력한 경우 예외 처리
val hasFilter = level != null || pageRange != null || origin != null || genre != null
val hasFilter = level != null || pageRange != null || !parsedOrigins.isNullOrEmpty() || !parsedGenres.isNullOrEmpty()
if (!hasFilter && !keyword.isNullOrBlank()) {
throw CustomException(ErrorCode.FILTER_REQUIRED, null)
}

// 유효성 검증
validateFilterParams(level, pageRange, origin, genre)
validateFilterParams(level, pageRange, parsedOrigins, parsedGenres)

val spec = Specification.where(BookSpecification.withLevel(level))
.and(BookSpecification.withPageRange(pageRange))
.and(BookSpecification.withOrigin(origin))
.and(BookSpecification.withGenre(genre))
.and(BookSpecification.withOrigins(parsedOrigins))
.and(BookSpecification.withGenres(parsedGenres))
.and(BookSpecification.withKeyword(keyword))
.and(BookSpecification.withCursor(cursor))

Expand All @@ -111,20 +115,24 @@ class BookQueryService(
private fun validateFilterParams(
level: Int?,
pageRange: String?,
origin: String?,
genre: String?
origins: List<String>?,
genres: List<String>?
) {
if (level != null && level !in VALID_LEVELS) {
throw CustomException(ErrorCode.INVALID_DIFFICULTY, null)
}
if (pageRange != null && pageRange !in VALID_PAGE_RANGES) {
throw CustomException(ErrorCode.INVALID_PAGE_RANGE, null)
}
if (origin != null && origin !in VALID_ORIGINS) {
throw CustomException(ErrorCode.INVALID_ORIGIN, null)
origins?.forEach { origin ->
if (origin !in VALID_ORIGINS) {
throw CustomException(ErrorCode.INVALID_ORIGIN, null)
}
}
if (genre != null && genre !in VALID_GENRES) {
throw CustomException(ErrorCode.INVALID_GENRE, null)
genres?.forEach { genre ->
if (genre !in VALID_GENRES) {
throw CustomException(ErrorCode.INVALID_GENRE, null)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,27 +45,27 @@ object BookSpecification {
}

/**
* 국가별 분류 필터
* 국가별 분류 필터 (복수 선택 가능 - OR 조건)
*/
fun withOrigin(origin: String?): Specification<Book> {
fun withOrigins(origins: List<String>?): Specification<Book> {
return Specification { root, _, cb ->
if (origin.isNullOrBlank()) {
if (origins.isNullOrEmpty()) {
null
} else {
cb.equal(root.get<String>("origin"), origin)
root.get<String>("origin").`in`(origins)
}
}
}

/**
* 장르별 분류 필터
* 장르별 분류 필터 (복수 선택 가능 - OR 조건)
*/
fun withGenre(genre: String?): Specification<Book> {
fun withGenres(genres: List<String>?): Specification<Book> {
return Specification { root, _, cb ->
if (genre.isNullOrBlank()) {
if (genres.isNullOrEmpty()) {
null
} else {
cb.equal(root.get<String>("genre"), genre)
root.get<String>("genre").`in`(genres)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,17 @@ class BookController(
## 필터 옵션
- **level**: 난이도 (1, 2, 3)
- **pageRange**: 분량 (~200, 250, 350, 500, 650, 651~) - 단일 선택, 선택한 값 이하 모두 포함
- **origin**: 국가별 (한국소설, 영미소설, 중국소설, 일본소설, 프랑스소설, 독일소설)
- **genre**: 장르별 (로맨스, 희곡, 무협소설, 판타지/환상문학, 역사소설, 라이트노벨, 추리/미스터리, 과학소설(SF), 액션/스릴러, 호러/공포소설)
- **origins**: 국가별 (한국소설, 영미소설, 중국소설, 일본소설, 프랑스소설, 독일소설) - 복수 선택 가능
- **genres**: 장르별 (로맨스, 희곡, 무협소설, 판타지/환상문학, 역사소설, 라이트노벨, 추리/미스터리, 과학소설(SF), 액션/스릴러, 호러/공포소설) - 복수 선택 가능
- **keyword**: 검색어 (제목, 저자, 출판사에서 검색) - 필터 선택 후 사용 가능

## 페이지네이션
- **cursor**: 마지막으로 조회한 bookId (첫 요청 시 생략)
- **hasNext**: 다음 페이지 존재 여부
- 정렬: id 오름차순

모든 필터는 선택 사항이며, 복수 필터 적용 시 AND 조건으로 검색됩니다.
모든 필터는 선택 사항이며, 서로 다른 필터 간에는 AND 조건으로 검색됩니다.
동일 필터 내 복수 선택 시 OR 조건으로 검색됩니다.
keyword 입력 시 필터링된 결과 내에서 추가로 검색됩니다.
유효하지 않은 필터 값을 입력하면 400 Bad Request 에러가 반환됩니다.
"""
Expand All @@ -86,12 +87,12 @@ class BookController(
fun filterBooks(
@Parameter(description = "난이도") @RequestParam(required = false) level: Int?,
@Parameter(description = "분량 (단일 선택: ~200, 250, 350, 500, 650, 651~)") @RequestParam(required = false) pageRange: String?,
@Parameter(description = "국가별 분류") @RequestParam(required = false) origin: String?,
@Parameter(description = "장르별 분류") @RequestParam(required = false) genre: String?,
@Parameter(description = "국가별 분류 (복수 선택 가능)") @RequestParam(required = false) origins: List<String>?,
@Parameter(description = "장르별 분류 (복수 선택 가능)") @RequestParam(required = false) genres: List<String>?,
@Parameter(description = "검색어 (제목, 저자, 출판사)") @RequestParam(required = false) keyword: String?,
@Parameter(description = "마지막으로 조회한 bookId (첫 요청 시 생략)") @RequestParam(required = false) cursor: Long?
): ResponseEntity<ApiResponse<BookFilterResponse>> {
val response = bookQueryService.filter(level, pageRange, origin, genre, keyword, cursor)
val response = bookQueryService.filter(level, pageRange, origins, genres, keyword, cursor)
return ResponseEntity.ok(ApiResponse.ok(response))
}
}