Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,11 @@ class BookController(
) : BookControllerApi {

@GetMapping("/search")
override fun searchBooks(@Valid @ModelAttribute request: BookSearchRequest): ResponseEntity<BookSearchResponse> {
val response = bookUseCase.searchBooks(request)
override fun searchBooks(
@AuthenticationPrincipal userId: UUID,
@Valid @ModelAttribute request: BookSearchRequest
): ResponseEntity<BookSearchResponse> {
val response = bookUseCase.searchBooks(request, userId)
return ResponseEntity.ok(response)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ import java.util.UUID
@RequestMapping("/api/v1/books")
interface BookControllerApi {

@Operation(summary = "도서 검색", description = "키워드를 사용하여 알라딘 도서 정보를 검색합니다.")
@Operation(
summary = "도서 검색", description = "알라딘 API를 통해 키워드로 도서를 검색합니다. \n" +
" 유저의 도서 상태(읽음, 읽는 중 등)가 함께 표시됩니다. "
)
@ApiResponses(
value = [
ApiResponse(
Expand All @@ -44,7 +47,10 @@ interface BookControllerApi {
]
)
@GetMapping("/search")
fun searchBooks(@Valid @Parameter(description = "도서 검색 요청 객체") request: BookSearchRequest): ResponseEntity<BookSearchResponse>
fun searchBooks(
@AuthenticationPrincipal userId: UUID,
@Valid @Parameter(description = "도서 검색 요청 객체") request: BookSearchRequest
): ResponseEntity<BookSearchResponse>

@Operation(summary = "도서 상세 조회", description = "특정 도서의 상세 정보를 조회합니다.")
@ApiResponses(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.yapp.apis.book.dto.response

import org.yapp.domain.userbook.BookStatus
import org.yapp.infra.external.aladin.response.AladinSearchResponse
import org.yapp.infra.external.aladin.response.BookItem

Expand Down Expand Up @@ -41,6 +42,7 @@ data class BookSearchResponse private constructor(
val author: String?,
val publisher: String?,
val coverImageUrl: String?,
var userBookStatus: BookStatus
) {
companion object {
private val unknownTitle = "제목없음"
Expand All @@ -52,7 +54,8 @@ data class BookSearchResponse private constructor(
title = item.title ?: unknownTitle,
author = item.author,
publisher = item.publisher,
coverImageUrl = item.cover
coverImageUrl = item.cover,
userBookStatus = BookStatus.BEFORE_READING
)
}
}
Expand Down
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.UserBook
import org.yapp.domain.userbook.vo.UserBookVO
import java.time.format.DateTimeFormatter
import java.util.*

Expand All @@ -18,10 +19,9 @@ data class UserBookResponse private constructor(
val updatedAt: String,
) {


companion object {
fun from(
userBook: UserBook,
): UserBookResponse {
fun from(userBook: UserBook): UserBookResponse {
return UserBookResponse(
userBookId = userBook.id,
userId = userBook.userId,
Expand All @@ -35,5 +35,20 @@ data class UserBookResponse private constructor(
updatedAt = userBook.updatedAt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
)
}

fun from(vo: UserBookVO): UserBookResponse {
return UserBookResponse(
userBookId = vo.id,
userId = vo.userId,
bookIsbn = vo.bookIsbn,
bookTitle = vo.title,
bookAuthor = vo.author,
status = vo.status,
coverImageUrl = vo.coverImageUrl,
publisher = vo.publisher,
createdAt = vo.createdAt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
updatedAt = vo.updatedAt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
)
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

팩터리 메서드가 올바르게 구현되었습니다.

새로 추가된 from(vo: UserBookVO) 메서드는 기존 from(userBook: UserBook) 메서드와 일관된 패턴을 따르고 있으며, 필드 매핑과 날짜 포맷팅이 적절히 구현되었습니다.

두 팩터리 메서드 간에 코드 중복이 있어 DRY 원칙을 위반하고 있지만, 메서드가 간단하므로 현재로서는 허용 가능한 수준입니다.

🤖 Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/book/dto/response/UserBookResponse.kt
around lines 39 to 52, the from(vo: UserBookVO) factory method is correctly
implemented and consistent with the existing from(userBook: UserBook) method. No
changes are needed as the code is acceptable despite minor duplication, so
approve the code changes as is.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import org.springframework.stereotype.Service
import org.yapp.apis.book.dto.request.BookCreateRequest
import org.yapp.domain.book.Book
import org.yapp.domain.book.BookDomainService
import java.util.UUID

@Service
class BookManagementService(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package org.yapp.apis.book.service

import org.springframework.stereotype.Service
import org.yapp.apis.book.dto.response.UserBookResponse
import org.yapp.domain.book.Book
import org.yapp.domain.userbook.UserBookDomainService
import org.yapp.domain.userbook.BookStatus
import java.util.*
import org.yapp.domain.userbook.vo.UserBookVO
import java.util.UUID

@Service
class UserBookService(
Expand All @@ -15,4 +17,11 @@ class UserBookService(

fun findAllUserBooks(userId: UUID) =
userBookDomainService.findAllUserBooks(userId)

fun findAllByUserIdAndBookIsbnIn(userId: UUID, isbns: List<String>): List<UserBookResponse> {
return userBookDomainService
.findAllByUserIdAndBookIsbnIn(userId, isbns).map { UserBookResponse.from(it) }
}

}

15 changes: 13 additions & 2 deletions apis/src/main/kotlin/org/yapp/apis/book/usecase/BookUseCase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,19 @@ class BookUseCase(
private val bookQueryService: BookQueryService,
private val bookManagementService: BookManagementService
) {
fun searchBooks(request: BookSearchRequest): BookSearchResponse {
return bookQueryService.searchBooks(request)
fun searchBooks(request: BookSearchRequest, userId: UUID): BookSearchResponse {
userAuthService.validateUserExists(userId)

val searchResponse = bookQueryService.searchBooks(request)
val isbns = searchResponse.books.map { it.isbn }
Copy link
Member

Choose a reason for hiding this comment

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

(사소) 요거 requestDTO로 포장해서 findAllByUserIdAndBookIsbnIn 메서드의 인자로 넘기면 좋을 것 같긴 합니다! (하지만 중요한 부분은 아닙니다 ㅎㅎ)

val userBooks = userBookService.findAllByUserIdAndBookIsbnIn(userId, isbns)
Copy link
Member

Choose a reason for hiding this comment

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

요거 ResponseDTO라 userBooksReponse로 명명하면 vo가 아니라는걸 명확히 알 수 있을 것 같습니다!

val statusMap = userBooks.associateBy({ it.bookIsbn }, { it.status })
searchResponse.books.forEach { bookSummary ->
statusMap[bookSummary.isbn]?.let { status ->
bookSummary.userBookStatus = status
}
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

매핑 로직 최적화 및 성능 개선 제안

현재 구현은 올바르게 작동하지만 다음과 같은 개선사항을 고려해볼 수 있습니다:

  1. associateBy를 사용한 맵 생성과 forEach를 통한 업데이트가 분리되어 있어 가독성이 떨어집니다.
  2. 추가적인 데이터베이스 쿼리로 인한 성능 영향을 고려해야 합니다.

다음과 같이 리팩토링을 고려해보세요:

-        val searchResponse = bookQueryService.searchBooks(request)
-        val isbns = searchResponse.books.map { it.isbn }
-        val userBooks = userBookService.findAllByUserIdAndBookIsbnIn(userId, isbns)
-        val statusMap = userBooks.associateBy({ it.bookIsbn }, { it.status })
-        searchResponse.books.forEach { bookSummary ->
-            statusMap[bookSummary.isbn]?.let { status ->
-                bookSummary.userBookStatus = status
-            }
-        }
-        return searchResponse
+        val searchResponse = bookQueryService.searchBooks(request)
+        
+        if (searchResponse.books.isNotEmpty()) {
+            val statusMap = userBookService.findAllByUserIdAndBookIsbnIn(
+                userId, 
+                searchResponse.books.map { it.isbn }
+            ).associateBy(UserBookResponse::bookIsbn, UserBookResponse::status)
+            
+            searchResponse.books.forEach { bookSummary ->
+                bookSummary.userBookStatus = statusMap[bookSummary.isbn]
+            }
+        }
+        
+        return searchResponse
🤖 Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/book/usecase/BookUseCase.kt around lines
33 to 40, the current code separately creates a map of user book statuses and
then iterates over books to update their status, which reduces readability and
may impact performance. Refactor by combining the mapping and updating steps
into a single operation, such as using a map or associate function that directly
updates each book's userBookStatus while iterating, to improve clarity and
potentially reduce overhead.

return searchResponse
}

fun getBookDetail(bookDetailRequest: BookDetailRequest): BookDetailResponse {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.yapp.domain.userbook

import org.yapp.domain.book.Book
import org.yapp.domain.userbook.vo.UserBookVO
import org.yapp.globalutils.annotation.DomainService
import java.util.UUID

Expand All @@ -23,4 +24,12 @@ class UserBookDomainService(
fun findAllUserBooks(userId: UUID): List<UserBook> {
return userBookRepository.findAllByUserId(userId)
}

fun findAllByUserIdAndBookIsbnIn(userId: UUID, isbns: List<String>): List<UserBookVO> {
if (isbns.isEmpty()) {
return emptyList()
}
return userBookRepository.findAllByUserIdAndBookIsbnIn(userId, isbns)
.map { UserBookVO.newInstance(it) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ interface UserBookRepository {
fun save(userBook: UserBook): UserBook

fun findAllByUserId(userId: UUID): List<UserBook>

fun findAllByUserIdAndBookIsbnIn(userId: UUID, bookIsbns: List<String>): List<UserBook>

}
39 changes: 39 additions & 0 deletions domain/src/main/kotlin/org/yapp/domain/userbook/vo/UserBookVO.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.yapp.domain.userbook.vo

import org.yapp.domain.userbook.BookStatus
import org.yapp.domain.userbook.UserBook
import java.time.LocalDateTime
import java.util.UUID

data class UserBookVO private constructor(
val id: UUID,
val userId: UUID,
val bookIsbn: String,
val coverImageUrl: String,
val publisher: String,
val title: String,
val author: String,
val status: BookStatus,
val createdAt: LocalDateTime,
val updatedAt: LocalDateTime
) {

companion object {
fun newInstance(
userBook: UserBook,
): UserBookVO {
return UserBookVO(
id = userBook.id,
userId = userBook.userId,
bookIsbn = userBook.bookIsbn,
coverImageUrl = userBook.coverImageUrl,
publisher = userBook.publisher,
title = userBook.title,
author = userBook.author,
status = userBook.status,
createdAt = userBook.createdAt,
updatedAt = userBook.updatedAt
)
}
}
}
26 changes: 26 additions & 0 deletions gateway/src/main/kotlin/org/yapp/gateway/config/SecurityConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.CorsConfigurationSource
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
import org.yapp.gateway.filter.JwtAuthenticationFilter
import org.yapp.gateway.jwt.JwtTokenProvider

Expand All @@ -28,6 +31,7 @@ class SecurityConfig(
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http
.csrf { it.disable() }
.cors { it.configurationSource(corsConfigurationSource()) }
.formLogin { it.disable() }
.httpBasic { it.disable() }
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
Expand All @@ -44,4 +48,26 @@ class SecurityConfig(

return http.build()
}

@Bean
fun corsConfigurationSource(): CorsConfigurationSource {
val config = CorsConfiguration()
config.allowedOrigins = listOf(
"http://localhost:8080",
"http://localhost:3000",
"http://api.qweasdzxc.shop",
"https://api.qweasdzxc.shop",
"https://www.qweasdzxc.shop",
"https://qweasdzxc.shop"
)
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

도메인명 불일치를 확인해주세요.

PR 목표에서 언급된 도메인(api.qwasdzxc.shop)과 코드의 도메인(api.qweasdzxc.shop) 사이에 철자 차이가 있습니다. 실제 프로덕션 도메인과 일치하는지 확인이 필요합니다.

다음 스크립트로 실제 도메인 사용량을 확인해보겠습니다:


🏁 Script executed:

#!/bin/bash
# 설명: 프로젝트 전체에서 도메인 사용 패턴 확인

# qwasdzxc vs qweasdzxc 도메인 패턴 검색
echo "=== qwasdzxc 도메인 패턴 ==="
rg "qwasdzxc\.shop" --type-add 'config:*.{yml,yaml,properties,kt,java}' -t config

echo -e "\n=== qweasdzxc 도메인 패턴 ==="
rg "qweasdzxc\.shop" --type-add 'config:*.{yml,yaml,properties,kt,java}' -t config

echo -e "\n=== 전체 도메인 설정 파일들 ==="
fd -e yml -e yaml -e properties | xargs grep -l "shop"

Length of output: 830


allowedOrigins 도메인 철자 오류 확인 및 수정 필요 (qweasdzxc → qwasdzxc)

코드에서 사용 중인 도메인이 qweasdzxc.shop으로, PR 목표에 적힌 실제 프로덕션 도메인 qwasdzxc.shop과 불일치합니다. 아래 파일에서 도메인 철자를 확인하고 수정해주세요.

  • gateway/src/main/kotlin/org/yapp/gateway/config/SecurityConfig.kt (55–62행)

제안 수정(diff 예시):

- "http://api.qweasdzxc.shop",
- "https://api.qweasdzxc.shop",
- "https://www.qweasdzxc.shop",
- "https://qweasdzxc.shop"
+ "http://api.qwasdzxc.shop",
+ "https://api.qwasdzxc.shop",
+ "https://www.qwasdzxc.shop",
+ "https://qwasdzxc.shop"
📝 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
config.allowedOrigins = listOf(
"http://localhost:8080",
"http://localhost:3000",
"http://api.qweasdzxc.shop",
"https://api.qweasdzxc.shop",
"https://www.qweasdzxc.shop",
"https://qweasdzxc.shop"
)
config.allowedOrigins = listOf(
"http://localhost:8080",
"http://localhost:3000",
"http://api.qwasdzxc.shop",
"https://api.qwasdzxc.shop",
"https://www.qwasdzxc.shop",
"https://qwasdzxc.shop"
)
🤖 Prompt for AI Agents
In gateway/src/main/kotlin/org/yapp/gateway/config/SecurityConfig.kt between
lines 55 and 62, the allowedOrigins list contains domain names with a typo:
"qweasdzxc.shop" should be corrected to "qwasdzxc.shop" to match the actual
production domain. Update all occurrences of "qweasdzxc.shop" in the
allowedOrigins list to "qwasdzxc.shop" to fix the domain spelling.

config.allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
config.allowedHeaders = listOf("*")
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

보안 고려사항: 헤더 허용 범위를 제한하는 것을 검토해주세요.

allowedHeaders = listOf("*")는 모든 헤더를 허용하므로 보안상 위험할 수 있습니다. 필요한 헤더만 명시적으로 허용하는 것이 더 안전합니다.

다음과 같이 필요한 헤더만 명시적으로 허용하는 것을 고려해보세요:

-        config.allowedHeaders = listOf("*")
+        config.allowedHeaders = listOf(
+            "Authorization",
+            "Content-Type",
+            "Accept",
+            "X-Requested-With",
+            "Cache-Control"
+        )
📝 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
config.allowedHeaders = listOf("*")
config.allowedHeaders = listOf(
"Authorization",
"Content-Type",
"Accept",
"X-Requested-With",
"Cache-Control"
)
🤖 Prompt for AI Agents
In gateway/src/main/kotlin/org/yapp/gateway/config/SecurityConfig.kt at line 64,
the allowedHeaders is set to allow all headers with "*", which poses a security
risk. Replace the wildcard with a list of only the specific headers your
application requires, such as "Authorization", "Content-Type", and any other
necessary headers, to restrict access and improve security.

config.exposedHeaders = listOf("Authorization", "Content-Type")
config.allowCredentials = true
config.maxAge = 3600L

val source = UrlBasedCorsConfigurationSource()
source.registerCorsConfiguration("/**", config)
return source
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ import java.util.*
interface JpaUserBookRepository : JpaRepository<UserBookEntity, UUID> {
fun findByUserIdAndBookIsbn(userId: UUID, bookIsbn: String): UserBookEntity?
fun findAllByUserId(userId: UUID): List<UserBookEntity>
fun findAllByUserIdAndBookIsbnIn(userId: UUID, bookIsbnList: List<String>): List<UserBookEntity>
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

JPA 메서드 명명 규칙을 올바르게 따르고 있습니다.

Spring Data JPA의 In 키워드를 사용한 메서드 시그니처가 올바르게 정의되었습니다. 다만 도메인 레이어에서는 bookIsbns를 사용하는데 여기서는 bookIsbnList를 사용하여 파라미터 명명에 일관성이 없습니다. 가능하다면 통일하는 것을 고려해보세요.

🤖 Prompt for AI Agents
In infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserBookRepository.kt
at line 10, the method parameter name bookIsbnList is inconsistent with the
domain layer's usage of bookIsbns. Rename the parameter from bookIsbnList to
bookIsbns to maintain naming consistency across layers.


}
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,12 @@ class UserBookRepositoryImpl(
override fun findAllByUserId(userId: UUID): List<UserBook> {
return jpaUserBookRepository.findAllByUserId(userId).map { it.toDomain() }
}

override fun findAllByUserIdAndBookIsbnIn(
userId: UUID,
bookIsbns: List<String>
): List<UserBook> {
return jpaUserBookRepository.findAllByUserIdAndBookIsbnIn(userId, bookIsbns)
.map { it.toDomain() }
}
}