Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ interface BookControllerApi {
),
ApiResponse(
responseCode = "404",
description = "존재하지 않는 책 (ISBN 오류)",
description = "존재하지 않는 책 (ISBN13 오류)",
content = [Content(schema = Schema(implementation = ErrorResponse::class))]
)
]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
package org.yapp.apis.book.dto.request

import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.Max
import jakarta.validation.constraints.Min
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Size
import jakarta.validation.constraints.*
import org.yapp.apis.book.dto.response.BookDetailResponse
import org.yapp.globalutils.util.RegexUtils

@Schema(
title = "책 생성 요청",
description = "시스템에 새로운 책 정보를 생성하는 요청 (주로 내부 API에서 사용)"
)
data class BookCreateRequest private constructor(
@field:NotBlank(message = "ISBN은 필수입니다.")
@field:NotBlank(message = "ISBN13은 필수입니다.")
@field:Pattern(
regexp = RegexUtils.ISBN13_PATTERN,
message = "유효한 13자리 ISBN13 형식이 아닙니다."
)
@Schema(
description = "책의 13자리 ISBN 코드",
description = "책의 13자리 ISBN13 코드",
example = "9788932473901",
required = true,
minLength = 13,
maxLength = 13
)
val isbn: String? = null,
val isbn13: String? = null,

@field:NotBlank(message = "제목은 필수입니다.")
@field:Size(max = 500, message = "제목은 500자 이내여야 합니다.")
Expand Down Expand Up @@ -81,26 +83,25 @@ data class BookCreateRequest private constructor(
)
val description: String? = null
) {
fun validIsbn(): String = isbn!!
fun validIsbn13(): String = isbn13!!
fun validTitle(): String = title!!
fun validAuthor(): String = author!!
fun validPublisher(): String = publisher!!
fun validCoverImageUrl(): String = coverImageUrl!!

companion object {

fun from(bookDetail: BookDetailResponse): BookCreateRequest {
val finalIsbn = bookDetail.isbn13
?: throw IllegalArgumentException("ISBN이 존재하지 않습니다.")
fun from(bookDetailResponse: BookDetailResponse): BookCreateRequest {
val finalIsbn13 = bookDetailResponse.isbn13
?: throw IllegalArgumentException("ISBN13이 존재하지 않습니다.")

return BookCreateRequest(
isbn = finalIsbn,
title = bookDetail.title,
author = bookDetail.author,
publisher = bookDetail.publisher,
publicationYear = parsePublicationYear(bookDetail.pubDate),
coverImageUrl = bookDetail.coverImageUrl,
description = bookDetail.description,
isbn13 = finalIsbn13,
title = bookDetailResponse.title,
author = bookDetailResponse.author,
publisher = bookDetailResponse.publisher,
publicationYear = parsePublicationYear(bookDetailResponse.pubDate),
coverImageUrl = bookDetailResponse.coverImageUrl,
description = bookDetailResponse.description,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,31 @@ import org.yapp.globalutils.util.RegexUtils

@Schema(
title = "책 상세 정보 요청",
description = "특정 ISBN을 통한 책 상세 정보 조회 요청"
description = "특정 ISBN13을 통한 책 상세 정보 조회 요청"
)
data class BookDetailRequest private constructor(
@field:NotBlank(message = "ISBN은 비어 있을 수 없습니다.")
@field:NotBlank(message = "ISBN13은 비어 있을 수 없습니다.")
@field:Pattern(
regexp = RegexUtils.ISBN13_PATTERN,
message = "유효한 13자리 ISBN 형식이 아닙니다."
message = "유효한 13자리 ISBN13 형식이 아닙니다."
)
@Schema(
description = "조회할 책의 13자리 ISBN 코드",
example = "9788932473901",
required = true,
pattern = "\\d{13}",
minLength = 13,
maxLength = 13
)
val isbn: String? = null,
val isbn13: String? = null,
) {
fun validIsbn(): String = isbn!!
fun validIsbn13(): String = isbn13!!

companion object {
fun from(
isbn: String?,
isbn13: String,
): BookDetailRequest {
return BookDetailRequest(
isbn = isbn,
isbn13 = isbn13,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package org.yapp.apis.book.dto.request
import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.NotNull
import jakarta.validation.constraints.Pattern
import jakarta.validation.constraints.Size
import org.yapp.apis.book.dto.response.BookCreateResponse
import org.yapp.domain.userbook.BookStatus
import org.yapp.globalutils.util.RegexUtils
import java.util.UUID

@Schema(
Expand All @@ -31,15 +33,19 @@ data class UpsertUserBookRequest private constructor(
)
val bookId: UUID? = null,

@field:NotBlank(message = "책 ISBN은 필수입니다.")
@field:NotBlank(message = "책 ISBN13은 필수입니다.")
@field:Pattern(
regexp = RegexUtils.ISBN13_PATTERN,
message = "유효한 13자리 ISBN13 형식이 아닙니다."
)
@Schema(
description = "책의 13자리 ISBN 코드",
description = "책의 13자리 ISBN13 코드",
example = "9788932473901",
required = true,
minLength = 13,
maxLength = 13
)
val bookIsbn: String? = null,
val isbn13: String? = null,

@field:NotBlank(message = "책 제목은 필수입니다.")
@field:Size(max = 500, message = "책 제목은 500자 이내여야 합니다.")
Expand Down Expand Up @@ -94,7 +100,7 @@ data class UpsertUserBookRequest private constructor(
) {
fun validUserId(): UUID = userId!!
fun validBookId(): UUID = bookId!!
fun validBookIsbn(): String = bookIsbn!!
fun validBookIsbn13(): String = isbn13!!
fun validBookTitle(): String = bookTitle!!
fun validBookAuthor(): String = bookAuthor!!
fun validBookPublisher(): String = bookPublisher!!
Expand All @@ -110,7 +116,7 @@ data class UpsertUserBookRequest private constructor(
return UpsertUserBookRequest(
userId = userId,
bookId = bookCreateResponse.bookId,
bookIsbn = bookCreateResponse.isbn,
isbn13 = bookCreateResponse.isbn13,
bookTitle = bookCreateResponse.title,
bookAuthor = bookCreateResponse.author,
bookPublisher = bookCreateResponse.publisher,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,28 @@ package org.yapp.apis.book.dto.request
import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.NotNull
import jakarta.validation.constraints.Pattern
import org.yapp.domain.userbook.BookStatus
import org.yapp.globalutils.util.RegexUtils

@Schema(
title = "사용자 도서 등록 요청",
description = "사용자의 서재에 도서를 등록하거나 상태를 변경하는 요청"
)
data class UserBookRegisterRequest private constructor(
@field:NotBlank(message = "ISBN은 필수입니다.")
@field:NotBlank(message = "ISBN13은 비어 있을 수 없습니다.")
@field:Pattern(
regexp = RegexUtils.ISBN13_PATTERN,
message = "유효한 13자리 ISBN13 형식이 아닙니다."
)
@Schema(
description = "등록할 책의 13자리 ISBN 코드",
description = "등록할 책의 13자리 ISBN13 코드",
example = "9788932473901",
required = true,
minLength = 13,
maxLength = 13
)
val bookIsbn: String? = null,
val isbn13: String? = null,

@field:NotNull(message = "도서 상태는 필수입니다.")
@Schema(
Expand All @@ -30,6 +36,6 @@ data class UserBookRegisterRequest private constructor(
)
val bookStatus: BookStatus? = null
) {
fun validBookIsbn(): String = bookIsbn!!
fun validIsbn13(): String = isbn13!!
fun validBookStatus(): BookStatus = bookStatus!!
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import jakarta.validation.constraints.NotNull
import java.util.UUID

@Schema(
name = "UserBooksByIsbnsRequest",
description = "Request DTO for finding user books by user ID and a list of ISBNs"
name = "UserBooksByIsbn13sRequest",
description = "Request DTO for finding user books by user ID and a list of ISBN13s"
)
data class UserBooksByIsbnsRequest private constructor(
data class UserBooksByIsbn13sRequest private constructor(
@Schema(
description = "사용자 ID",
example = "1"
Expand All @@ -21,16 +21,16 @@ data class UserBooksByIsbnsRequest private constructor(
description = "도서 ISBN 목록",
example = "[\"9788966262429\", \"9791190412351\"]"
)
@field:NotEmpty(message = "isbns는 비어있을 수 없습니다.")
val isbns: List<String>? = null
@field:NotEmpty(message = "isbn13 리스트는 비어있을 수 없습니다.")
val isbn13s: List<String>? = null
Comment on lines +24 to +25
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

검증 메시지 용어 일관성을 개선해주세요.

검증 메시지가 "isbn13 리스트"로 되어 있지만, 실제 프로퍼티명은 isbn13s입니다. 일관성을 위해 메시지를 프로퍼티명과 맞춰주세요.

다음과 같이 수정을 제안합니다:

-@field:NotEmpty(message = "isbn13 리스트는 비어있을 수 없습니다.")
+@field:NotEmpty(message = "isbn13s는 비어있을 수 없습니다.")
📝 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
@field:NotEmpty(message = "isbn13 리스트는 비어있을 수 없습니다.")
val isbn13s: List<String>? = null
@field:NotEmpty(message = "isbn13s는 비어있을 수 없습니다.")
val isbn13s: List<String>? = null
🤖 Prompt for AI Agents
In
apis/src/main/kotlin/org/yapp/apis/book/dto/request/UserBooksByIsbn13sRequest.kt
around lines 24 to 25, the validation message uses "isbn13 리스트" which is
inconsistent with the property name `isbn13s`. Update the validation message to
use `isbn13s` instead of "isbn13 리스트" to maintain terminology consistency.


) {
fun validUserId(): UUID = userId!!
fun validIsbns(): List<String> = isbns!!
fun validIsbn13s(): List<String> = isbn13s!!

companion object {
fun of(userId: UUID, isbns: List<String>): UserBooksByIsbnsRequest {
return UserBooksByIsbnsRequest(userId = userId, isbns = isbns)
fun of(userId: UUID, isbn13s: List<String>): UserBooksByIsbn13sRequest {
return UserBooksByIsbn13sRequest(userId = userId, isbn13s = isbn13s)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import java.util.UUID

data class BookCreateResponse private constructor(
val bookId: UUID,
val isbn: String,
val isbn13: String,
val title: String,
val author: String,
val publisher: String,
Expand All @@ -16,7 +16,7 @@ data class BookCreateResponse private constructor(
fun from(bookVO: BookInfoVO): BookCreateResponse {
return BookCreateResponse(
bookId = bookVO.id.value,
isbn = bookVO.isbn.value,
isbn13 = bookVO.isbn13.value,
title = bookVO.title,
author = bookVO.author,
publisher = bookVO.publisher,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import java.util.UUID
data class UserBookResponse private constructor(
val userBookId: UUID,
val userId: UUID,
val bookIsbn: String,
val isbn13: String,
val bookTitle: String,
val bookAuthor: String,
val status: BookStatus,
Expand All @@ -26,7 +26,7 @@ data class UserBookResponse private constructor(
return UserBookResponse(
userBookId = userBook.id.value,
userId = userBook.userId.value,
bookIsbn = userBook.bookIsbn.value,
isbn13 = userBook.bookIsbn13.value,
bookTitle = userBook.title,
bookAuthor = BookDataValidator.removeParenthesesFromAuthor(userBook.author),
status = userBook.status,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import org.yapp.infra.external.aladin.AladinApi
import org.yapp.infra.external.aladin.request.AladinBookLookupRequest
import org.yapp.infra.external.aladin.request.AladinBookSearchRequest
import org.yapp.infra.external.aladin.response.AladinBookDetailResponse
import org.yapp.infra.external.aladin.response.AladinSearchItem
import org.yapp.infra.external.aladin.response.AladinSearchResponse

@Service
Expand Down Expand Up @@ -46,12 +47,7 @@ class AladinBookQueryService(
}

val filteredItems = response.item.filter { item ->
val isbn13 = item.isbn13?.takeIf { it.isNotBlank() }
?: item.isbn?.let { IsbnConverter.toIsbn13(it) }

isbn13?.let {
IsbnValidator.isValidIsbn(it) && !it.startsWith("K", ignoreCase = true)
} ?: false
getValidAndFilteredIsbn13(item) != null
}

val filteredResponse = AladinSearchResponse(
Expand All @@ -73,15 +69,28 @@ class AladinBookQueryService(

override fun getBookDetail(@Valid request: BookDetailRequest): BookDetailResponse {
log.info("Service - Converting BookDetailRequest to AladinBookLookupRequest and calling Aladin API for book detail lookup.")
val aladinLookupRequest = AladinBookLookupRequest.from(request.validIsbn())
val aladinLookupRequest = AladinBookLookupRequest.from(request.validIsbn13())
val response: AladinBookDetailResponse = aladinApi.lookupBook(aladinLookupRequest)
.onSuccess { response ->
log.info("Aladin lookup successful for itemId: '${aladinLookupRequest.itemId}', title: ${response.item?.firstOrNull()?.title}")
log.info("Aladin lookup successful for itemId: '${aladinLookupRequest.itemId}', title: ${response.item.firstOrNull()?.title}")
}
.getOrElse { exception ->
log.error("Failed to call Aladin lookup API for request: '$request'", exception)
throw BookException(BookErrorCode.ALADIN_API_LOOKUP_FAILED, exception.message)
}
return BookDetailResponse.from(response)
}


private fun getValidAndFilteredIsbn13(item: AladinSearchItem): String? {
val primaryIsbn13 = item.isbn13
?.takeIf { it.isNotBlank() && IsbnValidator.isValidIsbn13(it) }

val convertedIsbn13 = item.isbn
?.takeIf { it.isNotBlank() && IsbnValidator.isValidIsbn10(it) }
?.let { validIsbn10 -> IsbnConverter.toIsbn13(validIsbn10) }
?.takeIf { IsbnValidator.isValidIsbn13(it) }

return primaryIsbn13 ?: convertedIsbn13
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class BookManagementService(
) {
fun findOrCreateBook(@Valid request: BookCreateRequest): BookCreateResponse {
val bookInfoVO = bookDomainService.findOrCreate(
request.validIsbn(),
request.validIsbn13(),
request.validTitle(),
request.validAuthor(),
request.validPublisher(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package org.yapp.apis.book.service

import jakarta.validation.Valid
import org.yapp.apis.book.dto.request.BookDetailRequest
import org.yapp.apis.book.dto.request.BookSearchRequest
import org.yapp.apis.book.dto.response.BookDetailResponse
import org.yapp.apis.book.dto.response.BookSearchResponse

sealed interface BookQueryService {
fun searchBooks(request: BookSearchRequest): BookSearchResponse
fun getBookDetail(@Valid request: BookDetailRequest): BookDetailResponse
fun getBookDetail(request: BookDetailRequest): BookDetailResponse
}
Loading