Skip to content

Commit 4865b60

Browse files
authored
feat: Guest용 도서 조회 API 구현 (#102)
* [BOOK-271] feat: apis - 비회원 검색 API 구현 * [BOOK-271] chore: apis - BookSearchResponse에 간단한 명세 변경 * [BOOK-271] feat: apis - 게스트용 도서 검색 비즈니스 로직 추가 * [BOOK-271] feat: gateway - whitelist에 guest 조회 API 추가
1 parent 450d0c2 commit 4865b60

File tree

6 files changed

+170
-20
lines changed

6 files changed

+170
-20
lines changed

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,7 @@ import org.springframework.web.bind.annotation.RestController
1818
import org.yapp.apis.book.dto.request.BookDetailRequest
1919
import org.yapp.apis.book.dto.request.BookSearchRequest
2020
import org.yapp.apis.book.dto.request.UserBookRegisterRequest
21-
import org.yapp.apis.book.dto.response.BookDetailResponse
22-
import org.yapp.apis.book.dto.response.BookSearchResponse
23-
import org.yapp.apis.book.dto.response.UserBookPageResponse
24-
import org.yapp.apis.book.dto.response.UserBookResponse
21+
import org.yapp.apis.book.dto.response.*
2522
import org.yapp.apis.book.usecase.BookUseCase
2623
import org.yapp.domain.userbook.BookStatus
2724
import org.yapp.domain.userbook.UserBookSortType
@@ -33,11 +30,20 @@ class BookController(
3330
private val bookUseCase: BookUseCase,
3431
) : BookControllerApi {
3532

33+
@GetMapping("/guest/search")
34+
override fun searchBooksForGuest(
35+
@Valid @ModelAttribute request: BookSearchRequest
36+
): ResponseEntity<GuestBookSearchResponse> {
37+
val response = bookUseCase.searchBooksForGuest(request)
38+
return ResponseEntity.ok(response)
39+
}
40+
3641
@GetMapping("/search")
3742
override fun searchBooks(
43+
@AuthenticationPrincipal userId: UUID,
3844
@Valid @ModelAttribute request: BookSearchRequest
3945
): ResponseEntity<BookSearchResponse> {
40-
val response = bookUseCase.searchBooks(request)
46+
val response = bookUseCase.searchBooks(request, userId)
4147
return ResponseEntity.ok(response)
4248
}
4349

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

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,7 @@ import org.springframework.web.bind.annotation.*
1717
import org.yapp.apis.book.dto.request.BookDetailRequest
1818
import org.yapp.apis.book.dto.request.BookSearchRequest
1919
import org.yapp.apis.book.dto.request.UserBookRegisterRequest
20-
import org.yapp.apis.book.dto.response.BookDetailResponse
21-
import org.yapp.apis.book.dto.response.BookSearchResponse
22-
import org.yapp.apis.book.dto.response.UserBookPageResponse
23-
import org.yapp.apis.book.dto.response.UserBookResponse
20+
import org.yapp.apis.book.dto.response.*
2421
import org.yapp.domain.userbook.BookStatus
2522
import org.yapp.domain.userbook.UserBookSortType
2623
import org.yapp.globalutils.exception.ErrorResponse
@@ -32,7 +29,30 @@ import java.util.*
3229
interface BookControllerApi {
3330

3431
@Operation(
35-
summary = "도서 검색", description = "알라딘 API를 통해 키워드로 도서를 검색합니다. \n" +
32+
summary = "비회원 도서 검색", description = "알라딘 API를 통해 키워드로 도서를 검색합니다. \n" +
33+
" 비회원이기에 도서 상태(읽음, 읽는 중 등)은 표시되지 않습니다. "
34+
)
35+
@ApiResponses(
36+
value = [
37+
ApiResponse(
38+
responseCode = "200",
39+
description = "성공적인 검색",
40+
content = [Content(schema = Schema(implementation = GuestBookSearchResponse::class))]
41+
),
42+
ApiResponse(
43+
responseCode = "400",
44+
description = "잘못된 요청 파라미터",
45+
content = [Content(schema = Schema(implementation = ErrorResponse::class))]
46+
)
47+
]
48+
)
49+
@GetMapping("/guest/search")
50+
fun searchBooksForGuest(
51+
@Valid @Parameter(description = "도서 검색 요청 객체") request: BookSearchRequest
52+
): ResponseEntity<GuestBookSearchResponse>
53+
54+
@Operation(
55+
summary = "회원 도서 검색", description = "알라딘 API를 통해 키워드로 도서를 검색합니다. \n" +
3656
" 유저의 도서 상태(읽음, 읽는 중 등)가 함께 표시됩니다. "
3757
)
3858
@ApiResponses(
@@ -51,6 +71,7 @@ interface BookControllerApi {
5171
)
5272
@GetMapping("/search")
5373
fun searchBooks(
74+
@AuthenticationPrincipal userId: UUID,
5475
@Valid @Parameter(description = "도서 검색 요청 객체") request: BookSearchRequest
5576
): ResponseEntity<BookSearchResponse>
5677

apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookSearchResponse.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ data class BookSearchResponse private constructor(
7878
}
7979
}
8080

81-
@Schema(name = "BookSummary", description = "검색된 단일 책 요약 정보")
81+
@Schema(name = "BookSummary", description = "회원용 검색된 단일 책 요약 정보")
8282
data class BookSummary private constructor(
8383

8484
@field:Schema(description = "ISBN-13 번호", example = "9781234567890")
@@ -103,7 +103,7 @@ data class BookSearchResponse private constructor(
103103
description = "알라딘 도서 상세 페이지 링크",
104104
example = "http://www.aladin.co.kr/shop/wproduct.aspx?ItemId=3680175"
105105
)
106-
val link: String, // Added link field
106+
val link: String,
107107

108108
@field:Schema(description = "사용자의 책 상태", example = "BEFORE_REGISTRATION")
109109
val userBookStatus: BookStatus
@@ -120,7 +120,7 @@ data class BookSearchResponse private constructor(
120120
author: String?,
121121
publisher: String?,
122122
coverImageUrl: String,
123-
link: String // Added link
123+
link: String
124124
): BookSummary {
125125
require(!title.isNullOrBlank()) { "Title is required" }
126126

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package org.yapp.apis.book.dto.response
2+
3+
import io.swagger.v3.oas.annotations.media.Schema
4+
import org.yapp.globalutils.validator.BookDataValidator
5+
6+
@Schema(
7+
name = "GuestBookSearchResponse",
8+
description = "게스트용 알라딘 도서 검색 API 응답"
9+
)
10+
data class GuestBookSearchResponse private constructor(
11+
@field:Schema(description = "API 응답 버전", example = "20131101")
12+
val version: String?,
13+
14+
@field:Schema(description = "검색 결과 제목", example = "데미안")
15+
val title: String?,
16+
17+
@field:Schema(description = "출간일", example = "2025-07-30")
18+
val pubDate: String?,
19+
20+
@field:Schema(description = "총 검색 결과 개수", example = "42")
21+
val totalResults: Int?,
22+
23+
@field:Schema(description = "검색 시작 인덱스", example = "1")
24+
val startIndex: Int?,
25+
26+
@field:Schema(description = "한 페이지당 검색 결과 개수", example = "10")
27+
val itemsPerPage: Int?,
28+
29+
@field:Schema(description = "검색 쿼리 문자열", example = "데미안")
30+
val query: String?,
31+
32+
@field:Schema(description = "검색 카테고리 ID", example = "1")
33+
val searchCategoryId: Int?,
34+
35+
@field:Schema(description = "검색 카테고리 이름", example = "소설/시/희곡")
36+
val searchCategoryName: String?,
37+
38+
@field:Schema(description = "마지막 페이지 여부", example = "false")
39+
val lastPage: Boolean,
40+
41+
@field:Schema(description = "검색된 책 목록 (게스트용)")
42+
val books: List<GuestBookSummary>
43+
) {
44+
companion object {
45+
fun from(response: BookSearchResponse): GuestBookSearchResponse {
46+
return GuestBookSearchResponse(
47+
version = response.version,
48+
title = response.title,
49+
pubDate = response.pubDate,
50+
totalResults = response.totalResults,
51+
startIndex = response.startIndex,
52+
itemsPerPage = response.itemsPerPage,
53+
query = response.query,
54+
searchCategoryId = response.searchCategoryId,
55+
searchCategoryName = response.searchCategoryName,
56+
lastPage = response.lastPage,
57+
books = response.books.map { userBookSummary ->
58+
GuestBookSummary.of(
59+
isbn13 = userBookSummary.isbn13,
60+
title = userBookSummary.title,
61+
author = userBookSummary.author,
62+
publisher = userBookSummary.publisher,
63+
coverImageUrl = userBookSummary.coverImageUrl,
64+
link = userBookSummary.link
65+
)
66+
}
67+
)
68+
}
69+
}
70+
71+
@Schema(name = "GuestBookSummary", description = "게스트용 검색된 단일 책 요약 정보")
72+
data class GuestBookSummary private constructor(
73+
74+
@field:Schema(description = "ISBN-13 번호", example = "9781234567890")
75+
val isbn13: String,
76+
77+
@field:Schema(description = "책 제목", example = "데미안")
78+
val title: String,
79+
80+
@field:Schema(description = "저자", example = "헤르만 헤세")
81+
val author: String?,
82+
83+
@field:Schema(description = "출판사", example = "민음사")
84+
val publisher: String?,
85+
86+
@field:Schema(
87+
description = "책 표지 이미지 URL",
88+
example = "https://image.aladin.co.kr/product/36801/75/coversum/k692030806_1.jpg"
89+
)
90+
val coverImageUrl: String,
91+
92+
@field:Schema(
93+
description = "알라딘 도서 상세 페이지 링크",
94+
example = "http://www.aladin.co.kr/shop/wproduct.aspx?ItemId=3680175"
95+
)
96+
val link: String
97+
) {
98+
companion object {
99+
fun of(
100+
isbn13: String,
101+
title: String,
102+
author: String?,
103+
publisher: String?,
104+
coverImageUrl: String,
105+
link: String
106+
): GuestBookSummary {
107+
return GuestBookSummary(
108+
isbn13 = isbn13,
109+
title = title,
110+
author = author,
111+
publisher = publisher?.let { BookDataValidator.removeParenthesesFromPublisher(it) },
112+
coverImageUrl = coverImageUrl,
113+
link = link
114+
)
115+
}
116+
}
117+
}
118+
}

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,8 @@ import org.springframework.beans.factory.annotation.Qualifier
55
import org.springframework.data.domain.Pageable
66
import org.springframework.transaction.annotation.Transactional
77
import org.yapp.apis.book.dto.request.*
8-
import org.yapp.apis.book.dto.response.BookDetailResponse
9-
import org.yapp.apis.book.dto.response.BookSearchResponse
8+
import org.yapp.apis.book.dto.response.*
109
import org.yapp.apis.book.dto.response.BookSearchResponse.BookSummary
11-
import org.yapp.apis.book.dto.response.UserBookPageResponse
12-
import org.yapp.apis.book.dto.response.UserBookResponse
1310
import org.yapp.apis.book.service.BookManagementService
1411
import org.yapp.apis.book.service.BookQueryService
1512
import org.yapp.apis.book.service.UserBookService
@@ -30,11 +27,19 @@ class BookUseCase(
3027
private val bookManagementService: BookManagementService,
3128
private val readingRecordService: ReadingRecordService
3229
) {
33-
fun searchBooks(
30+
fun searchBooksForGuest(
3431
request: BookSearchRequest
32+
): GuestBookSearchResponse {
33+
val searchResponse = bookQueryService.searchBooks(request)
34+
return GuestBookSearchResponse.from(searchResponse)
35+
}
36+
37+
fun searchBooks(
38+
request: BookSearchRequest,
39+
userId: UUID
3540
): BookSearchResponse {
3641
val searchResponse = bookQueryService.searchBooks(request)
37-
val booksWithUserStatus = mergeWithUserBookStatus(searchResponse.books, null)
42+
val booksWithUserStatus = mergeWithUserBookStatus(searchResponse.books, userId)
3843

3944
return searchResponse.withUpdatedBooks(booksWithUserStatus)
4045
}

gateway/src/main/kotlin/org/yapp/gateway/security/SecurityConfig.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class SecurityConfig(
2222
) {
2323
companion object {
2424
private val WHITELIST_URLS = arrayOf(
25-
"/api/v1/books/search",
25+
"/api/v1/books/guest/search",
2626
"/api/v1/auth/refresh",
2727
"/api/v1/auth/signin",
2828
"/actuator/**",

0 commit comments

Comments
 (0)