-
Notifications
You must be signed in to change notification settings - Fork 1
feat: Guest용 도서 조회 API 구현 #102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
26a7ab7
6d5fba6
179046e
de1d7b0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,10 +17,7 @@ import org.springframework.web.bind.annotation.* | |
| 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.dto.response.* | ||
| import org.yapp.domain.userbook.BookStatus | ||
| import org.yapp.domain.userbook.UserBookSortType | ||
| import org.yapp.globalutils.exception.ErrorResponse | ||
|
|
@@ -32,7 +29,30 @@ import java.util.* | |
| interface BookControllerApi { | ||
|
|
||
| @Operation( | ||
| summary = "도서 검색", description = "알라딘 API를 통해 키워드로 도서를 검색합니다. \n" + | ||
| summary = "비회원 도서 검색", description = "알라딘 API를 통해 키워드로 도서를 검색합니다. \n" + | ||
| " 비회원이기에 도서 상태(읽음, 읽는 중 등)은 표시되지 않습니다. " | ||
| ) | ||
| @ApiResponses( | ||
| value = [ | ||
| ApiResponse( | ||
| responseCode = "200", | ||
| description = "성공적인 검색", | ||
| content = [Content(schema = Schema(implementation = GuestBookSearchResponse::class))] | ||
| ), | ||
| ApiResponse( | ||
| responseCode = "400", | ||
| description = "잘못된 요청 파라미터", | ||
| content = [Content(schema = Schema(implementation = ErrorResponse::class))] | ||
| ) | ||
| ] | ||
| ) | ||
| @GetMapping("/guest/search") | ||
| fun searchBooksForGuest( | ||
| @Valid @Parameter(description = "도서 검색 요청 객체") request: BookSearchRequest | ||
| ): ResponseEntity<GuestBookSearchResponse> | ||
|
|
||
|
Comment on lines
31
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) 게스트용 API 스펙 명료화 좋습니다 게스트 응답 DTO를 별도 분리하고 비회원 제약(도서 상태 비노출)을 명확히 기재한 점 좋습니다. 페이지네이션 필드(lastPage, itemsPerPage 등)가 응답에 포함됨을 문서에도 간단히 언급하면 소비자 입장에서 더 명확해집니다. 🤖 Prompt for AI Agents |
||
| @Operation( | ||
| summary = "회원 도서 검색", description = "알라딘 API를 통해 키워드로 도서를 검색합니다. \n" + | ||
| " 유저의 도서 상태(읽음, 읽는 중 등)가 함께 표시됩니다. " | ||
| ) | ||
| @ApiResponses( | ||
|
|
@@ -51,6 +71,7 @@ interface BookControllerApi { | |
| ) | ||
| @GetMapping("/search") | ||
| fun searchBooks( | ||
| @AuthenticationPrincipal userId: UUID, | ||
| @Valid @Parameter(description = "도서 검색 요청 객체") request: BookSearchRequest | ||
| ): ResponseEntity<BookSearchResponse> | ||
|
Comment on lines
72
to
76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) 회원 검색에 대한 보안 스키마 노출(스웨거) 제안 실제 보안은 잘 적용되어 있지만, 스웨거 문서에도 보안 요구 사항을 노출하면 더 명확합니다. springdoc 기준으로 SecurityRequirement를 추가하세요(스키마 이름은 프로젝트 설정에 맞춰 조정 필요). 예시: @Operation(
summary = "...",
security = [SecurityRequirement(name = "bearerAuth")]
)🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,118 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package org.yapp.apis.book.dto.response | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import io.swagger.v3.oas.annotations.media.Schema | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.yapp.globalutils.validator.BookDataValidator | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Schema( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name = "GuestBookSearchResponse", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description = "게스트용 알라딘 도서 검색 API 응답" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data class GuestBookSearchResponse private constructor( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @field:Schema(description = "API 응답 버전", example = "20131101") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val version: String?, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @field:Schema(description = "검색 결과 제목", example = "데미안") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val title: String?, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @field:Schema(description = "출간일", example = "2025-07-30") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val pubDate: String?, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @field:Schema(description = "총 검색 결과 개수", example = "42") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val totalResults: Int?, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @field:Schema(description = "검색 시작 인덱스", example = "1") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val startIndex: Int?, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @field:Schema(description = "한 페이지당 검색 결과 개수", example = "10") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val itemsPerPage: Int?, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @field:Schema(description = "검색 쿼리 문자열", example = "데미안") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val query: String?, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @field:Schema(description = "검색 카테고리 ID", example = "1") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val searchCategoryId: Int?, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @field:Schema(description = "검색 카테고리 이름", example = "소설/시/희곡") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val searchCategoryName: String?, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @field:Schema(description = "마지막 페이지 여부", example = "false") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val lastPage: Boolean, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @field:Schema(description = "검색된 책 목록 (게스트용)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val books: List<GuestBookSummary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+38
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) OpenAPI 스키마에서 필수 필드 표시 게스트 응답의 - @field:Schema(description = "마지막 페이지 여부", example = "false")
+ @field:Schema(description = "마지막 페이지 여부", example = "false", requiredMode = Schema.RequiredMode.REQUIRED)
val lastPage: Boolean,
- @field:Schema(description = "검색된 책 목록 (게스트용)")
+ @field:Schema(description = "검색된 책 목록 (게스트용)", requiredMode = Schema.RequiredMode.REQUIRED)
val books: List<GuestBookSummary>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| companion object { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fun from(response: BookSearchResponse): GuestBookSearchResponse { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return GuestBookSearchResponse( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| version = response.version, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title = response.title, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pubDate = response.pubDate, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| totalResults = response.totalResults, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| startIndex = response.startIndex, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| itemsPerPage = response.itemsPerPage, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| query = response.query, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| searchCategoryId = response.searchCategoryId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| searchCategoryName = response.searchCategoryName, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastPage = response.lastPage, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| books = response.books.map { userBookSummary -> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| GuestBookSummary.of( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isbn13 = userBookSummary.isbn13, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title = userBookSummary.title, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| author = userBookSummary.author, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| publisher = userBookSummary.publisher, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| coverImageUrl = userBookSummary.coverImageUrl, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| link = userBookSummary.link | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+57
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) 변수명 간결화 제안: userBookSummary → book 게스트 DTO 변환 컨텍스트에서 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+45
to
+68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion null-안전성과 경계값 보강 필요: lastPage, books 기본값 처리 소스
다음과 같이 보강을 제안합니다: fun from(response: BookSearchResponse): GuestBookSearchResponse {
- return GuestBookSearchResponse(
+ // nullable 대비: lastPage 계산/기본값
+ val computedLastPage = response.lastPage ?: run {
+ val total = response.totalResults
+ val start = response.startIndex
+ val size = response.itemsPerPage
+ if (total != null && start != null && size != null) {
+ start + size >= total
+ } else {
+ false
+ }
+ }
+ return GuestBookSearchResponse(
version = response.version,
title = response.title,
pubDate = response.pubDate,
totalResults = response.totalResults,
startIndex = response.startIndex,
itemsPerPage = response.itemsPerPage,
query = response.query,
searchCategoryId = response.searchCategoryId,
searchCategoryName = response.searchCategoryName,
- lastPage = response.lastPage,
- books = response.books.map { userBookSummary ->
+ lastPage = computedLastPage,
+ books = response.books.orEmpty().map { book ->
- GuestBookSummary.of(
- isbn13 = userBookSummary.isbn13,
- title = userBookSummary.title,
- author = userBookSummary.author,
- publisher = userBookSummary.publisher,
- coverImageUrl = userBookSummary.coverImageUrl,
- link = userBookSummary.link
+ GuestBookSummary.of(
+ isbn13 = book.isbn13,
+ title = book.title,
+ author = book.author,
+ publisher = book.publisher,
+ coverImageUrl = book.coverImageUrl,
+ link = book.link
)
}
)
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+44
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) 단위 테스트 추가 제안: 매핑/정규화 검증 게스트 DTO 팩토리의 핵심 로직이므로, 최소한 다음 케이스에 대한 단위 테스트를 권장합니다:
필요하시면 테스트 스캐폴딩을 생성해 드리겠습니다. 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Schema(name = "GuestBookSummary", description = "게스트용 검색된 단일 책 요약 정보") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data class GuestBookSummary private constructor( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @field:Schema(description = "ISBN-13 번호", example = "9781234567890") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val isbn13: String, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @field:Schema(description = "책 제목", example = "데미안") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val title: String, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @field:Schema(description = "저자", example = "헤르만 헤세") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val author: String?, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @field:Schema(description = "출판사", example = "민음사") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val publisher: String?, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @field:Schema( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description = "책 표지 이미지 URL", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| example = "https://image.aladin.co.kr/product/36801/75/coversum/k692030806_1.jpg" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val coverImageUrl: String, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @field:Schema( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description = "알라딘 도서 상세 페이지 링크", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| example = "http://www.aladin.co.kr/shop/wproduct.aspx?ItemId=3680175" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+93
to
+95
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) 예시 링크 https 권장 문서 예시 값은 가급적 https를 권장합니다. - example = "http://www.aladin.co.kr/shop/wproduct.aspx?ItemId=3680175"
+ example = "https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=3680175"📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val link: String | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| companion object { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fun of( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isbn13: String, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title: String, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| author: String?, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| publisher: String?, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| coverImageUrl: String, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| link: String | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): GuestBookSummary { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return GuestBookSummary( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isbn13 = isbn13, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title = title, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| author = author, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| publisher = publisher?.let { BookDataValidator.removeParenthesesFromPublisher(it) }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| coverImageUrl = coverImageUrl, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| link = link | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+98
to
+116
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) publisher 정규화 추가 보강: trim/blank 처리 출판사 값이 공백이거나 공백만 있는 경우 null로 정규화하면 응답 품질이 좋아집니다. 현재 괄호 제거만 하므로 최소한의 전처리를 더하는 것을 제안합니다. - return GuestBookSummary(
- isbn13 = isbn13,
- title = title,
- author = author,
- publisher = publisher?.let { BookDataValidator.removeParenthesesFromPublisher(it) },
- coverImageUrl = coverImageUrl,
- link = link
- )
+ val normalizedPublisher = publisher
+ ?.trim()
+ ?.takeIf { it.isNotBlank() }
+ ?.let(BookDataValidator::removeParenthesesFromPublisher)
+ return GuestBookSummary(
+ isbn13 = isbn13,
+ title = title,
+ author = author,
+ publisher = normalizedPublisher,
+ coverImageUrl = coverImageUrl,
+ link = link
+ )📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,11 +5,8 @@ import org.springframework.beans.factory.annotation.Qualifier | |
| import org.springframework.data.domain.Pageable | ||
| import org.springframework.transaction.annotation.Transactional | ||
| import org.yapp.apis.book.dto.request.* | ||
| import org.yapp.apis.book.dto.response.BookDetailResponse | ||
| import org.yapp.apis.book.dto.response.BookSearchResponse | ||
| import org.yapp.apis.book.dto.response.* | ||
| import org.yapp.apis.book.dto.response.BookSearchResponse.BookSummary | ||
| import org.yapp.apis.book.dto.response.UserBookPageResponse | ||
| import org.yapp.apis.book.dto.response.UserBookResponse | ||
| import org.yapp.apis.book.service.BookManagementService | ||
| import org.yapp.apis.book.service.BookQueryService | ||
| import org.yapp.apis.book.service.UserBookService | ||
|
|
@@ -30,11 +27,19 @@ class BookUseCase( | |
| private val bookManagementService: BookManagementService, | ||
| private val readingRecordService: ReadingRecordService | ||
| ) { | ||
| fun searchBooks( | ||
| fun searchBooksForGuest( | ||
| request: BookSearchRequest | ||
| ): GuestBookSearchResponse { | ||
| val searchResponse = bookQueryService.searchBooks(request) | ||
| return GuestBookSearchResponse.from(searchResponse) | ||
| } | ||
|
|
||
| fun searchBooks( | ||
| request: BookSearchRequest, | ||
| userId: UUID | ||
| ): BookSearchResponse { | ||
| val searchResponse = bookQueryService.searchBooks(request) | ||
| val booksWithUserStatus = mergeWithUserBookStatus(searchResponse.books, null) | ||
| val booksWithUserStatus = mergeWithUserBookStatus(searchResponse.books, userId) | ||
|
|
||
| return searchResponse.withUpdatedBooks(booksWithUserStatus) | ||
| } | ||
|
Comment on lines
+37
to
45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) 회원 검색에서 사용자 검증/널 처리 일관성 및 단순화 제안
아래와 같이 단순화할 수 있습니다. - private fun mergeWithUserBookStatus(
- searchedBooks: List<BookSummary>,
- userId: UUID?
- ): List<BookSummary> {
- if (userId == null || searchedBooks.isEmpty()) {
- return searchedBooks
- }
+ private fun mergeWithUserBookStatus(
+ searchedBooks: List<BookSummary>,
+ userId: UUID
+ ): List<BookSummary> {
+ if (searchedBooks.isEmpty()) return searchedBooks
val isbn13s = searchedBooks.map { it.isbn13 }
val userBookStatusMap = getUserBookStatusMap(isbn13s, userId)
return searchedBooks.map { bookSummary ->
userBookStatusMap[bookSummary.isbn13]
?.let { bookSummary.updateStatus(it) }
?: bookSummary
}
}
- private fun getUserBookStatusMap(
- isbn13s: List<String>,
- userId: UUID?
- ): Map<String, BookStatus> {
- if (userId == null) return emptyMap()
+ private fun getUserBookStatusMap(
+ isbn13s: List<String>,
+ userId: UUID
+ ): Map<String, BookStatus> {
val userBooksResponse = userBookService.findAllByUserIdAndBookIsbn13In(
UserBooksByIsbn13sRequest.of(userId, isbn13s)
)
return userBooksResponse.associate { userBook ->
userBook.isbn13 to userBook.status
}
}사용자 존재 검증을 넣을지 여부는 아래와 같이 선택적으로 적용 가능합니다. fun searchBooks(request: BookSearchRequest, userId: UUID): BookSearchResponse {
// 정책에 따라 활성화
// userService.validateUserExists(userId)
val searchResponse = bookQueryService.searchBooks(request)
val booksWithUserStatus = mergeWithUserBookStatus(searchResponse.books, userId)
return searchResponse.withUpdatedBooks(booksWithUserStatus)
}🤖 Prompt for AI Agents |
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,7 +22,7 @@ class SecurityConfig( | |
| ) { | ||
| companion object { | ||
| private val WHITELIST_URLS = arrayOf( | ||
| "/api/v1/books/search", | ||
| "/api/v1/books/guest/search", | ||
| "/api/v1/auth/refresh", | ||
|
Comment on lines
24
to
26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) permitAll 경로에서 잘못된 토큰에 의한 401 가능성 점검 Spring Security에서 BearerTokenAuthenticationFilter는 Authorization 헤더가 존재하면 인증을 시도합니다. 게스트 공개 엔드포인트라도 잘못된 토큰이 실린 요청은 401이 날 수 있으니, UX 정책에 따라 다음 중 하나를 고려해 주세요.
필터를 추가하는 예시는 아래와 같습니다. // 새 필터 추가 예시 (별도 파일로 생성)
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletRequestWrapper
import org.springframework.util.AntPathMatcher
import org.springframework.web.filter.OncePerRequestFilter
import java.util.*
class SkipBearerOnWhitelistFilter(
private val whitelist: Array<String>
) : OncePerRequestFilter() {
private val matcher = AntPathMatcher()
override fun shouldNotFilter(request: HttpServletRequest): Boolean {
return whitelist.none { matcher.match(it, request.requestURI) }
}
override fun doFilterInternal(request: HttpServletRequest, response: jakarta.servlet.http.HttpServletResponse, filterChain: jakarta.servlet.FilterChain) {
val wrapped = object : HttpServletRequestWrapper(request) {
override fun getHeader(name: String?): String? {
return if (name.equals("Authorization", ignoreCase = true)) null else super.getHeader(name)
}
override fun getHeaders(name: String?): Enumeration<String> {
if (name.equals("Authorization", ignoreCase = true)) return Collections.emptyEnumeration()
return super.getHeaders(name)
}
}
filterChain.doFilter(wrapped, response)
}
}SecurityConfig 에 필터를 BearerTokenAuthenticationFilter 이전에 추가: .authorizeHttpRequests {
it.requestMatchers(*WHITELIST_URLS).permitAll()
it.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
it.anyRequest().authenticated()
}
+.addFilterBefore(SkipBearerOnWhitelistFilter(WHITELIST_URLS), BearerTokenAuthenticationFilter::class.java)
.addFilterAfter(mdcLoggingFilter, BearerTokenAuthenticationFilter::class.java)🤖 Prompt for AI Agents |
||
| "/api/v1/auth/signin", | ||
| "/actuator/**", | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
게스트 검색 엔드포인트 추가 적절 + 남용 방지 전략 고려
API 분리는 명확합니다. 공개 엔드포인트인 만큼 트래픽 남용(스크래핑, 대량 호출) 대비를 위해 게이트웨이/인프라 레벨에서의 레이트 리밋, 캐시 전략 등을 함께 검토해 주세요.
🤖 Prompt for AI Agents