diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/service/TokenService.kt b/apis/src/main/kotlin/org/yapp/apis/auth/service/TokenService.kt index 7249f8a3..93b49fde 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/service/TokenService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/service/TokenService.kt @@ -1,6 +1,8 @@ package org.yapp.apis.auth.service +import jakarta.validation.Valid import org.springframework.stereotype.Service +import org.springframework.validation.annotation.Validated import org.yapp.apis.auth.dto.request.TokenGenerateRequest import org.yapp.apis.auth.dto.request.TokenRefreshRequest import org.yapp.apis.auth.dto.response.RefreshTokenResponse @@ -9,6 +11,7 @@ import org.yapp.domain.token.TokenDomainRedisService import java.util.* @Service +@Validated class TokenService( private val tokenDomainRedisService: TokenDomainRedisService, ) { @@ -16,7 +19,7 @@ class TokenService( tokenDomainRedisService.deleteRefreshTokenByToken(token) } - fun saveRefreshToken(tokenGenerateRequest: TokenGenerateRequest): RefreshTokenResponse { + fun saveRefreshToken(@Valid tokenGenerateRequest: TokenGenerateRequest): RefreshTokenResponse { val token = tokenDomainRedisService.saveRefreshToken( tokenGenerateRequest.validUserId(), tokenGenerateRequest.validRefreshToken(), diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/service/UserAuthService.kt b/apis/src/main/kotlin/org/yapp/apis/auth/service/UserAuthService.kt index e4845c27..6e9160bc 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/service/UserAuthService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/service/UserAuthService.kt @@ -1,6 +1,8 @@ package org.yapp.apis.auth.service +import jakarta.validation.Valid import org.springframework.stereotype.Service +import org.springframework.validation.annotation.Validated import org.yapp.apis.auth.dto.request.FindOrCreateUserRequest import org.yapp.apis.auth.dto.request.FindUserIdentityRequest import org.yapp.apis.auth.dto.response.CreateUserResponse @@ -13,6 +15,7 @@ import org.yapp.domain.user.vo.UserIdentityVO import java.util.* @Service +@Validated class UserAuthService( private val userDomainService: UserDomainService ) { @@ -33,12 +36,12 @@ class UserAuthService( } } - fun findUserIdentityByUserId(findUserIdentityRequest: FindUserIdentityRequest): UserAuthInfoResponse { + fun findUserIdentityByUserId(@Valid findUserIdentityRequest: FindUserIdentityRequest): UserAuthInfoResponse { val userIdentity = userDomainService.findUserIdentityById(findUserIdentityRequest.validUserId()) return UserAuthInfoResponse.from(userIdentity) } - fun findOrCreateUser(findOrCreateUserRequest: FindOrCreateUserRequest): CreateUserResponse { + fun findOrCreateUser(@Valid findOrCreateUserRequest: FindOrCreateUserRequest): CreateUserResponse { userDomainService.findUserByProviderTypeAndProviderId( findOrCreateUserRequest.validProviderType(), findOrCreateUserRequest.validProviderId() diff --git a/apis/src/main/kotlin/org/yapp/apis/book/constant/BookQueryServiceQualifier.kt b/apis/src/main/kotlin/org/yapp/apis/book/constant/BookQueryServiceQualifier.kt deleted file mode 100644 index 778576d9..00000000 --- a/apis/src/main/kotlin/org/yapp/apis/book/constant/BookQueryServiceQualifier.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.yapp.apis.book.constant - -object BookQueryServiceQualifier { - const val ALADIN = "aladinBookQueryService" -} diff --git a/apis/src/main/kotlin/org/yapp/apis/book/controller/BookController.kt b/apis/src/main/kotlin/org/yapp/apis/book/controller/BookController.kt index a036e959..7696d250 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/controller/BookController.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/controller/BookController.kt @@ -45,7 +45,7 @@ class BookController( @AuthenticationPrincipal userId: UUID, @Valid @ModelAttribute request: BookDetailRequest ): ResponseEntity { - val response = bookUseCase.getBookDetail(request) + val response = bookUseCase.getBookDetail(request, userId) return ResponseEntity.ok(response) } diff --git a/apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookCreateRequest.kt b/apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookCreateRequest.kt index 62afb3bb..bb89d0f0 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookCreateRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookCreateRequest.kt @@ -1,38 +1,96 @@ 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 org.yapp.apis.book.dto.response.BookDetailResponse +@Schema( + title = "책 생성 요청", + description = "시스템에 새로운 책 정보를 생성하는 요청 (주로 내부 API에서 사용)" +) data class BookCreateRequest private constructor( @field:NotBlank(message = "ISBN은 필수입니다.") + @Schema( + description = "책의 13자리 ISBN 코드", + example = "9788932473901", + required = true, + minLength = 13, + maxLength = 13 + ) val isbn: String? = null, @field:NotBlank(message = "제목은 필수입니다.") + @field:Size(max = 500, message = "제목은 500자 이내여야 합니다.") + @Schema( + description = "책 제목", + example = "해리 포터와 마법사의 돌", + required = true, + maxLength = 500 + ) val title: String? = null, @field:NotBlank(message = "저자는 필수입니다.") + @field:Size(max = 200, message = "저자는 200자 이내여야 합니다.") + @Schema( + description = "저자명 (여러 저자인 경우 쉼표로 구분)", + example = "J.K. 롤링", + required = true, + maxLength = 200 + ) val author: String? = null, @field:NotBlank(message = "출판사는 필수입니다.") + @field:Size(max = 200, message = "출판사는 200자 이내여야 합니다.") + @Schema( + description = "출판사명", + example = "문학수첩", + required = true, + maxLength = 200 + ) val publisher: String? = null, + @field:Min(value = 1000, message = "출간연도는 1000년 이후여야 합니다.") + @field:Max(value = 2100, message = "출간연도는 2100년 이전이어야 합니다.") + @Schema( + description = "출간연도 (4자리 년도)", + example = "2000", + minimum = "1000", + maximum = "2100" + ) val publicationYear: Int? = null, @field:Size(max = 2048, message = "표지 URL은 2048자 이내여야 합니다.") - val coverImageUrl: String, + @field:NotBlank(message = "표지 이미지 URL은 필수입니다.") + @Schema( + description = "책 표지 이미지 URL", + example = "https://image.aladin.co.kr/product/123/45/cover/1234567890123.jpg", + required = true, + maxLength = 2048, + format = "uri" + ) + val coverImageUrl: String? = null, + @field:Size(max = 2000, message = "책 설명은 2000자 이내여야 합니다.") + @Schema( + description = "책 소개 및 줄거리", + example = "11살 해리 포터는 이모네 집에서 갖은 구박을 당하며 지낸다...", + maxLength = 2000 + ) val description: String? = null ) { fun validIsbn(): String = isbn!! 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.isbn ?: bookDetail.isbn13 + val finalIsbn = bookDetail.isbn13 ?: throw IllegalArgumentException("ISBN이 존재하지 않습니다.") return BookCreateRequest( @@ -41,7 +99,7 @@ data class BookCreateRequest private constructor( author = bookDetail.author, publisher = bookDetail.publisher, publicationYear = parsePublicationYear(bookDetail.pubDate), - coverImageUrl = bookDetail.cover, + coverImageUrl = bookDetail.coverImageUrl, description = bookDetail.description, ) } diff --git a/apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookDetailRequest.kt b/apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookDetailRequest.kt index 8efa078c..d8bb18fa 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookDetailRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookDetailRequest.kt @@ -1,38 +1,38 @@ package org.yapp.apis.book.dto.request +import io.swagger.v3.oas.annotations.media.Schema import jakarta.validation.constraints.NotBlank -import jakarta.validation.constraints.Pattern // Pattern 어노테이션 추가 +import jakarta.validation.constraints.Pattern import org.yapp.globalutils.util.RegexUtils -import org.yapp.infra.external.aladin.dto.AladinBookLookupRequest +@Schema( + title = "책 상세 정보 요청", + description = "특정 ISBN을 통한 책 상세 정보 조회 요청" +) data class BookDetailRequest private constructor( - @field:NotBlank(message = "아이템 ID는 필수입니다.") + @field:NotBlank(message = "ISBN은 비어 있을 수 없습니다.") @field:Pattern( - regexp = RegexUtils.NOT_BLANK_AND_NOT_NULL_STRING_PATTERN, - message = "아이템 ID는 유효한 ISBN 형식이 아닙니다." + regexp = RegexUtils.ISBN13_PATTERN, + message = "유효한 13자리 ISBN 형식이 아닙니다." ) - val itemId: String? = null, - val itemIdType: String? = "ISBN", - val optResult: List? = null + @Schema( + description = "조회할 책의 13자리 ISBN 코드", + example = "9788932473901", + required = true, + pattern = "\\d{13}", + minLength = 13, + maxLength = 13 + ) + val isbn: String? = null, ) { - - fun validIsbn(): String = itemId!! - - - fun toAladinRequest(): AladinBookLookupRequest { - return AladinBookLookupRequest.create( - itemId = this.itemId!!, - itemIdType = this.itemIdType ?: "ISBN", - optResult = this.optResult - ) - } + fun validIsbn(): String = isbn!! companion object { - fun of(isbn: String?, optResult: List? = null): BookDetailRequest { + fun from( + isbn: String?, + ): BookDetailRequest { return BookDetailRequest( - itemId = isbn, - itemIdType = "ISBN", - optResult = optResult + isbn = isbn, ) } } diff --git a/apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookSearchRequest.kt b/apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookSearchRequest.kt index 6fb7a7ff..c6e20006 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookSearchRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookSearchRequest.kt @@ -1,31 +1,73 @@ package org.yapp.apis.book.dto.request -import org.yapp.infra.external.aladin.dto.AladinBookSearchRequest - +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank +@Schema( + title = "책 검색 요청", + description = "알라딘 API를 통한 책 검색 요청 정보" +) data class BookSearchRequest private constructor( + @field:NotBlank(message = "검색어는 필수입니다.") + @Schema( + description = "검색할 키워드 (제목, 저자, 출판사 등)", + example = "해리포터", + required = true + ) val query: String? = null, + + @Schema( + description = "검색 유형", + example = "Title", + allowableValues = ["Title", "Author", "Publisher", "Keyword"], + defaultValue = "Keyword" + ) val queryType: String? = null, + + @Schema( + description = "검색 대상", + example = "Book", + allowableValues = ["Book", "Foreign", "Music", "DVD"], + defaultValue = "Book" + ) val searchTarget: String? = null, + + @field:Min(value = 1, message = "최대 결과 수는 1 이상이어야 합니다.") + @field:Max(value = 100, message = "최대 결과 수는 100 이하여야 합니다.") + @Schema( + description = "한 번에 가져올 최대 결과 수 (1-100)", + example = "10", + minimum = "1", + maximum = "100", + defaultValue = "10" + ) val maxResults: Int? = null, + + @field:Min(value = 1, message = "시작 인덱스는 1 이상이어야 합니다.") + @Schema( + description = "검색 시작 인덱스 (페이징)", + example = "1", + minimum = "1", + defaultValue = "1" + ) val start: Int? = null, + + @Schema( + description = "정렬 방식", + example = "Accuracy", + allowableValues = ["Accuracy", "PublishTime", "Title", "SalesPoint"], + defaultValue = "Accuracy" + ) val sort: String? = null, - val cover: String? = null, + + @Schema( + description = "카테고리 ID (0: 전체)", + example = "0", + defaultValue = "0" + ) val categoryId: Int? = null ) { - fun validQuery(): String = query!! - fun toAladinRequest(): AladinBookSearchRequest { - - return AladinBookSearchRequest.create( - query = this.validQuery(), - queryType = this.queryType, - searchTarget = this.searchTarget, - maxResults = this.maxResults, - start = this.start, - sort = this.sort, - cover = this.cover, - categoryId = this.categoryId - ) - } } diff --git a/apis/src/main/kotlin/org/yapp/apis/book/dto/request/UpsertUserBookRequest.kt b/apis/src/main/kotlin/org/yapp/apis/book/dto/request/UpsertUserBookRequest.kt index cd987615..fd466fa5 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/dto/request/UpsertUserBookRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/dto/request/UpsertUserBookRequest.kt @@ -1,18 +1,95 @@ 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.Size import org.yapp.apis.book.dto.response.BookCreateResponse import org.yapp.domain.userbook.BookStatus import java.util.UUID - +@Schema( + title = "사용자 도서 생성/수정 요청", + description = "사용자 서재에 도서를 생성하거나 기존 도서 정보를 수정하는 내부 API 요청 (주로 내부 서비스에서 사용)" +) data class UpsertUserBookRequest private constructor( + @field:NotNull(message = "사용자 ID는 필수입니다.") + @Schema( + description = "사용자 고유 식별자", + example = "550e8400-e29b-41d4-a716-446655440000", + required = true, + format = "uuid" + ) val userId: UUID? = null, + + @field:NotNull(message = "책 ID는 필수입니다.") + @Schema( + description = "책 고유 식별자", + example = "550e8400-e29b-41d4-a716-446655440001", + required = true, + format = "uuid" + ) val bookId: UUID? = null, + + @field:NotBlank(message = "책 ISBN은 필수입니다.") + @Schema( + description = "책의 13자리 ISBN 코드", + example = "9788932473901", + required = true, + minLength = 13, + maxLength = 13 + ) val bookIsbn: String? = null, + + @field:NotBlank(message = "책 제목은 필수입니다.") + @field:Size(max = 500, message = "책 제목은 500자 이내여야 합니다.") + @Schema( + description = "책 제목", + example = "해리 포터와 마법사의 돌", + required = true, + maxLength = 500 + ) val bookTitle: String? = null, + + @field:NotBlank(message = "저자는 필수입니다.") + @field:Size(max = 200, message = "저자는 200자 이내여야 합니다.") + @Schema( + description = "저자명", + example = "J.K. 롤링", + required = true, + maxLength = 200 + ) val bookAuthor: String? = null, + + @field:NotBlank(message = "출판사는 필수입니다.") + @field:Size(max = 200, message = "출판사는 200자 이내여야 합니다.") + @Schema( + description = "출판사명", + example = "문학수첩", + required = true, + maxLength = 200 + ) val bookPublisher: String? = null, + + @field:NotBlank(message = "표지 이미지 URL은 필수입니다.") + @field:Size(max = 2048, message = "표지 이미지 URL은 2048자 이내여야 합니다.") + @Schema( + description = "책 표지 이미지 URL", + example = "https://image.aladin.co.kr/product/123/45/cover/1234567890123.jpg", + required = true, + maxLength = 2048, + format = "uri" + ) val bookCoverImageUrl: String? = null, + + @field:NotNull(message = "도서 상태는 필수입니다.") + @Schema( + description = "사용자의 도서 읽기 상태", + example = "READING", + required = true, + allowableValues = ["BEFORE_REGISTRATION", "BEFORE_READING", "READING", "COMPLETED"], + enumAsRef = true + ) val status: BookStatus? = null ) { fun validUserId(): UUID = userId!! diff --git a/apis/src/main/kotlin/org/yapp/apis/book/dto/request/UserBookRegisterRequest.kt b/apis/src/main/kotlin/org/yapp/apis/book/dto/request/UserBookRegisterRequest.kt index be008096..ef140ed1 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/dto/request/UserBookRegisterRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/dto/request/UserBookRegisterRequest.kt @@ -1,20 +1,35 @@ 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 org.yapp.domain.userbook.BookStatus - +@Schema( + title = "사용자 도서 등록 요청", + description = "사용자의 서재에 도서를 등록하거나 상태를 변경하는 요청" +) data class UserBookRegisterRequest private constructor( @field:NotBlank(message = "ISBN은 필수입니다.") + @Schema( + description = "등록할 책의 13자리 ISBN 코드", + example = "9788932473901", + required = true, + minLength = 13, + maxLength = 13 + ) val bookIsbn: String? = null, - val bookStatus: BookStatus + @field:NotNull(message = "도서 상태는 필수입니다.") + @Schema( + description = "사용자의 도서 읽기 상태", + example = "READING", + required = true, + allowableValues = ["BEFORE_REGISTRATION", "BEFORE_READING", "READING", "COMPLETED"], + enumAsRef = true + ) + val bookStatus: BookStatus? = null ) { fun validBookIsbn(): String = bookIsbn!! - - companion object { - fun create(bookIsbn: String, bookStatus: BookStatus): UserBookRegisterRequest { - return UserBookRegisterRequest(bookIsbn, bookStatus) - } - } + fun validBookStatus(): BookStatus = bookStatus!! } diff --git a/apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookDetailResponse.kt b/apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookDetailResponse.kt index 63bcf923..14011d0c 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookDetailResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookDetailResponse.kt @@ -1,58 +1,50 @@ package org.yapp.apis.book.dto.response +import org.yapp.apis.util.AuthorExtractor +import org.yapp.apis.util.IsbnConverter +import org.yapp.domain.userbook.BookStatus import org.yapp.infra.external.aladin.response.AladinBookDetailResponse -import java.math.BigDecimal -/** - * 단일 도서의 상세 정보를 나타내는 DTO. - * 외부 API 응답 및 도메인 Book 객체로부터 변환됩니다. - */ data class BookDetailResponse private constructor( val version: String?, val title: String, - val link: String?, - val author: String, - val pubDate: String?, - val description: String?, - val isbn: String?, + val link: String, + val author: String?, + val pubDate: String, + val description: String, val isbn13: String?, - val itemId: Long?, - val priceSales: BigDecimal?, - val priceStandard: BigDecimal?, - val mallType: String?, - val stockStatus: String?, - val mileage: Int?, - val cover: String, - val categoryId: Int?, - val categoryName: String?, - val publisher: String, + val mallType: String, + val coverImageUrl: String, + val categoryName: String, + val publisher: String?, + val totalPage: Int?, + val userBookStatus: BookStatus ) { - companion object { - + fun withUserBookStatus(newUserBookStatus: BookStatus): BookDetailResponse { + return this.copy(userBookStatus = newUserBookStatus) + } - fun from(response: AladinBookDetailResponse): BookDetailResponse { - val bookItem = response.item?.firstOrNull() + companion object { + private const val DEFAULT_MAX_PAGE_COUNT = 4032 + + fun from(response: AladinBookDetailResponse, userBookStatus: BookStatus = BookStatus.BEFORE_REGISTRATION): BookDetailResponse { + val item = response.item.firstOrNull() ?: throw IllegalArgumentException("No book item found in detail response.") return BookDetailResponse( version = response.version, - title = bookItem.title ?: "", - link = bookItem.link, - author = bookItem.author ?: "", - pubDate = bookItem.pubDate, - description = bookItem.description ?: "", - isbn = bookItem.isbn ?: bookItem.isbn13 ?: "", - isbn13 = bookItem.isbn13, - itemId = bookItem.itemId, - priceSales = bookItem.priceSales, - priceStandard = bookItem.priceStandard, - mallType = bookItem.mallType, - stockStatus = bookItem.stockStatus, - mileage = bookItem.mileage, - cover = bookItem.cover ?: "", - categoryId = bookItem.categoryId, - categoryName = bookItem.categoryName, - publisher = bookItem.publisher ?: "", + title = item.title, + link = item.link, + author = AuthorExtractor.extractAuthors(item.author), + pubDate = item.pubDate ?: "", + description = item.description ?: "", + mallType = item.mallType, + isbn13 = item.isbn13 ?: IsbnConverter.toIsbn13(item.isbn) ?: throw IllegalArgumentException("Either isbn13 or isbn must be provided"), + coverImageUrl = item.cover, + categoryName = item.categoryName, + publisher = item.publisher ?: "", + totalPage = item.subInfo.itemPage ?: DEFAULT_MAX_PAGE_COUNT, + userBookStatus = userBookStatus ) } } diff --git a/apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookSearchResponse.kt b/apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookSearchResponse.kt index 4a75bb9d..7dbfd7f9 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookSearchResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookSearchResponse.kt @@ -1,10 +1,9 @@ package org.yapp.apis.book.dto.response +import org.yapp.apis.util.AuthorExtractor import org.yapp.domain.userbook.BookStatus -import org.yapp.domain.userbook.UserBook +import org.yapp.apis.util.IsbnConverter import org.yapp.infra.external.aladin.response.AladinSearchResponse -import org.yapp.infra.external.aladin.response.BookItem -import java.time.LocalDateTime data class BookSearchResponse private constructor( val version: String?, @@ -19,25 +18,12 @@ data class BookSearchResponse private constructor( val searchCategoryName: String?, val books: List ) { - fun from(updatedBooks: List): BookSearchResponse { - return BookSearchResponse( - version = this.version, - title = this.title, - link = this.link, - pubDate = this.pubDate, - totalResults = this.totalResults, - startIndex = this.startIndex, - itemsPerPage = this.itemsPerPage, - query = this.query, - searchCategoryId = this.searchCategoryId, - searchCategoryName = this.searchCategoryName, - books = updatedBooks - ) + fun withUpdatedBooks(updatedBooks: List): BookSearchResponse { + return this.copy(books = updatedBooks) } companion object { fun from(response: AladinSearchResponse): BookSearchResponse { - val books = response.item?.mapNotNull { BookSummary.fromAladinItem(it) } ?: emptyList() return BookSearchResponse( version = response.version, title = response.title, @@ -49,17 +35,26 @@ data class BookSearchResponse private constructor( query = response.query, searchCategoryId = response.searchCategoryId, searchCategoryName = response.searchCategoryName, - books = books + books = response.item.map { + BookSummary.of( + isbn = it.isbn, + isbn13 = it.isbn13, + title = it.title, + author = AuthorExtractor.extractAuthors(it.author), + publisher = it.publisher, + coverImageUrl = it.cover + ) + } ) } } data class BookSummary private constructor( - val isbn: String, + val isbn13: String, val title: String, val author: String?, val publisher: String?, - val coverImageUrl: String?, + val coverImageUrl: String, val userBookStatus: BookStatus ) { fun updateStatus(newStatus: BookStatus): BookSummary { @@ -67,17 +62,22 @@ data class BookSearchResponse private constructor( } companion object { - private val unknownTitle = "제목없음" + fun of( + isbn: String?, + isbn13: String?, + title: String?, + author: String?, + publisher: String?, + coverImageUrl: String + ): BookSummary { + require(!title.isNullOrBlank()) { "Title is required" } - - fun fromAladinItem(item: BookItem): BookSummary? { - val isbn = item.isbn ?: item.isbn13 ?: return null return BookSummary( - isbn = isbn, - title = item.title ?: unknownTitle, - author = item.author, - publisher = item.publisher, - coverImageUrl = item.cover, + isbn13 = isbn13 ?: IsbnConverter.toIsbn13(isbn) ?: throw IllegalArgumentException("Either isbn13 or isbn must be provided"), + title = title, + author = author, + publisher = publisher, + coverImageUrl = coverImageUrl, userBookStatus = BookStatus.BEFORE_REGISTRATION ) } diff --git a/apis/src/main/kotlin/org/yapp/apis/book/exception/BookErrorCode.kt b/apis/src/main/kotlin/org/yapp/apis/book/exception/BookErrorCode.kt index b662c3b9..2034b7ff 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/exception/BookErrorCode.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/exception/BookErrorCode.kt @@ -9,13 +9,9 @@ enum class BookErrorCode( private val code: String, private val message: String ) : BaseErrorCode { - - /* 404 NOT_FOUND */ - BOOK_NOT_FOUND(HttpStatus.NOT_FOUND, "BOOK_001", "Book not found."), - /* 500 INTERNAL_SERVER_ERROR */ - ALADIN_API_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "BOOK_002", "Error fetching book from external API."); - + ALADIN_API_SEARCH_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "BOOK_001", "알라딘 도서 검색 API 호출에 실패했습니다."), + ALADIN_API_LOOKUP_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "BOOK_002", "알라딘 도서 상세 조회 API 호출에 실패했습니다."); override fun getHttpStatus(): HttpStatus = httpStatus override fun getCode(): String = code diff --git a/apis/src/main/kotlin/org/yapp/apis/book/exception/UserBookNotFoundException.kt b/apis/src/main/kotlin/org/yapp/apis/book/exception/UserBookException.kt similarity index 85% rename from apis/src/main/kotlin/org/yapp/apis/book/exception/UserBookNotFoundException.kt rename to apis/src/main/kotlin/org/yapp/apis/book/exception/UserBookException.kt index 7598e468..5070455d 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/exception/UserBookNotFoundException.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/exception/UserBookException.kt @@ -2,7 +2,7 @@ package org.yapp.apis.book.exception import org.yapp.globalutils.exception.CommonException -class UserBookNotFoundException( +class UserBookException ( errorCode: UserBookErrorCode, message: String? = null ) : CommonException(errorCode, message) diff --git a/apis/src/main/kotlin/org/yapp/apis/book/helper/AladinApiHelper.kt b/apis/src/main/kotlin/org/yapp/apis/book/helper/AladinApiHelper.kt deleted file mode 100644 index dd759b0f..00000000 --- a/apis/src/main/kotlin/org/yapp/apis/book/helper/AladinApiHelper.kt +++ /dev/null @@ -1,45 +0,0 @@ -package org.yapp.infra.external.aladin.helper - -import mu.KotlinLogging -import org.yapp.globalutils.annotation.Helper -import org.yapp.infra.external.aladin.AladinApi -import org.yapp.infra.external.aladin.dto.AladinBookLookupRequest // Import Aladin DTOs -import org.yapp.infra.external.aladin.dto.AladinBookSearchRequest // Import Aladin DTOs -import org.yapp.infra.external.aladin.response.AladinBookDetailResponse -import org.yapp.infra.external.aladin.response.AladinSearchResponse - - -@Helper -class AladinApiHelper( - private val aladinApi: AladinApi -) { - private val log = KotlinLogging.logger {} - - fun searchBooks(request: AladinBookSearchRequest): AladinSearchResponse { - return aladinApi.searchBooks(request) // Pass the DTO directly to AladinApi - .onSuccess { response -> - log.info("Aladin search successful for query: '${request.query}', total results: ${response.totalResults}") - } - .getOrElse { exception -> - log.error("Failed to call Aladin search API for request: '$request'", exception) - throw IllegalStateException( - "Failed to retrieve search results from Aladin API: ${exception.message}", - exception - ) - } - } - - fun lookupBook(request: AladinBookLookupRequest): AladinBookDetailResponse { - return aladinApi.lookupBook(request) // Pass the DTO directly to AladinApi - .onSuccess { response -> - log.info("Aladin lookup successful for itemId: '${request.itemId}', title: ${response.item?.firstOrNull()?.title}") - } - .getOrElse { exception -> - log.error("Failed to call Aladin lookup API for request: '$request'", exception) - throw IllegalStateException( - "Failed to retrieve book details from Aladin API: ${exception.message}", - exception - ) - } - } -} diff --git a/apis/src/main/kotlin/org/yapp/apis/book/service/AladinBookQueryService.kt b/apis/src/main/kotlin/org/yapp/apis/book/service/AladinBookQueryService.kt new file mode 100644 index 00000000..02ca2d99 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/book/service/AladinBookQueryService.kt @@ -0,0 +1,61 @@ +package org.yapp.apis.book.service + +import jakarta.validation.Valid +import mu.KotlinLogging +import org.springframework.stereotype.Service +import org.springframework.validation.annotation.Validated +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 +import org.yapp.apis.book.exception.BookErrorCode +import org.yapp.apis.book.exception.BookException +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.AladinSearchResponse + +@Service +@Validated +class AladinBookQueryService( + private val aladinApi: AladinApi +) : BookQueryService { + private val log = KotlinLogging.logger {} + + override fun searchBooks(request: BookSearchRequest): BookSearchResponse { + log.info("Service - Converting BookSearchRequest to AladinBookSearchRequest and calling Aladin API for book search.") + val aladinSearchRequest = AladinBookSearchRequest.of( + request.validQuery(), + request.queryType, + request.searchTarget, + request.maxResults, + request.start, + request.sort, + request.categoryId + ) + val response: AladinSearchResponse = aladinApi.searchBooks(aladinSearchRequest) + .onSuccess { response -> + log.info("Aladin search successful for query: '${request.query}', total results: ${response.totalResults}") + } + .getOrElse { exception -> + log.error("Failed to call Aladin search API for request: '$request'", exception) + throw BookException(BookErrorCode.ALADIN_API_SEARCH_FAILED, exception.message) + } + return BookSearchResponse.from(response) + } + + 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 response: AladinBookDetailResponse = aladinApi.lookupBook(aladinLookupRequest) + .onSuccess { response -> + 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) + } +} diff --git a/apis/src/main/kotlin/org/yapp/apis/book/service/BookManagementService.kt b/apis/src/main/kotlin/org/yapp/apis/book/service/BookManagementService.kt index b7c6df90..559b118b 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/service/BookManagementService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/service/BookManagementService.kt @@ -1,21 +1,24 @@ package org.yapp.apis.book.service +import jakarta.validation.Valid import org.springframework.stereotype.Service +import org.springframework.validation.annotation.Validated import org.yapp.apis.book.dto.request.BookCreateRequest import org.yapp.apis.book.dto.response.BookCreateResponse import org.yapp.domain.book.BookDomainService @Service +@Validated class BookManagementService( private val bookDomainService: BookDomainService ) { - fun findOrCreateBook(request: BookCreateRequest): BookCreateResponse { + fun findOrCreateBook(@Valid request: BookCreateRequest): BookCreateResponse { val bookInfoVO = bookDomainService.findOrCreate( request.validIsbn(), request.validTitle(), request.validAuthor(), request.validPublisher(), - request.coverImageUrl, + request.validCoverImageUrl(), request.publicationYear, request.description ) diff --git a/apis/src/main/kotlin/org/yapp/apis/book/service/BookQueryService.kt b/apis/src/main/kotlin/org/yapp/apis/book/service/BookQueryService.kt index 01a8b89a..cc1527e4 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/service/BookQueryService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/service/BookQueryService.kt @@ -1,12 +1,12 @@ 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 - -interface BookQueryService { +sealed interface BookQueryService { fun searchBooks(request: BookSearchRequest): BookSearchResponse - fun getBookDetail(request: BookDetailRequest): BookDetailResponse + fun getBookDetail(@Valid request: BookDetailRequest): BookDetailResponse } diff --git a/apis/src/main/kotlin/org/yapp/apis/book/service/UserBookService.kt b/apis/src/main/kotlin/org/yapp/apis/book/service/UserBookService.kt index b2742c46..4a318616 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/service/UserBookService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/service/UserBookService.kt @@ -1,25 +1,27 @@ package org.yapp.apis.book.service +import jakarta.validation.Valid import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.stereotype.Service +import org.springframework.validation.annotation.Validated import org.yapp.apis.auth.dto.request.UserBooksByIsbnsRequest import org.yapp.apis.book.dto.request.UpsertUserBookRequest import org.yapp.apis.book.dto.response.UserBookPageResponse import org.yapp.apis.book.dto.response.UserBookResponse import org.yapp.apis.book.exception.UserBookErrorCode -import org.yapp.apis.book.exception.UserBookNotFoundException +import org.yapp.apis.book.exception.UserBookException import org.yapp.domain.userbook.BookStatus -import org.yapp.domain.userbook.UserBook import org.yapp.domain.userbook.UserBookDomainService import org.yapp.domain.userbook.UserBookSortType import java.util.* @Service +@Validated class UserBookService( private val userBookDomainService: UserBookDomainService ) { - fun upsertUserBook(upsertUserBookRequest: UpsertUserBookRequest): UserBookResponse { + fun upsertUserBook(@Valid upsertUserBookRequest: UpsertUserBookRequest): UserBookResponse { val userBookInfoVO = userBookDomainService.upsertUserBook( upsertUserBookRequest.validUserId(), upsertUserBookRequest.validBookId(), @@ -33,15 +35,16 @@ class UserBookService( return UserBookResponse.from(userBookInfoVO) } - fun validateUserBookExists(userId: UUID, userBookId: UUID): UserBook { - return userBookDomainService.findByIdAndUserId(userBookId, userId) - ?: throw UserBookNotFoundException( + fun validateUserBookExists(userBookId: UUID, userId: UUID) { + if (!userBookDomainService.existsByUserBookIdAndUserId(userBookId, userId)) { + throw UserBookException( UserBookErrorCode.USER_BOOK_NOT_FOUND, - "User book not found with id: $userBookId and userId: $userId" + "UserBook not found or access denied: $userBookId" ) + } } - fun findAllByUserIdAndBookIsbnIn(userBooksByIsbnsRequest: UserBooksByIsbnsRequest): List { + fun findAllByUserIdAndBookIsbnIn(@Valid userBooksByIsbnsRequest: UserBooksByIsbnsRequest): List { val userBooks = userBookDomainService.findAllByUserIdAndBookIsbnIn( userBooksByIsbnsRequest.validUserId(), userBooksByIsbnsRequest.validIsbns(), @@ -49,7 +52,10 @@ class UserBookService( return userBooks.map { UserBookResponse.from(it) } } - + fun findUserBookStatusByIsbn(userId: UUID, isbn: String): BookStatus? { + val userBook = userBookDomainService.findByUserIdAndBookIsbn(userId, isbn) + return userBook?.status + } private fun findUserBooksByDynamicCondition( userId: UUID, diff --git a/apis/src/main/kotlin/org/yapp/apis/book/service/impl/AladinBookQueryService.kt b/apis/src/main/kotlin/org/yapp/apis/book/service/impl/AladinBookQueryService.kt deleted file mode 100644 index 4aa47c28..00000000 --- a/apis/src/main/kotlin/org/yapp/apis/book/service/impl/AladinBookQueryService.kt +++ /dev/null @@ -1,37 +0,0 @@ -package org.yapp.apis.book.service.impl - -import mu.KotlinLogging -import org.springframework.beans.factory.annotation.Qualifier -import org.springframework.stereotype.Service -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 -import org.yapp.apis.book.constant.BookQueryServiceQualifier -import org.yapp.apis.book.service.BookQueryService -import org.yapp.infra.external.aladin.helper.AladinApiHelper -import org.yapp.infra.external.aladin.response.AladinBookDetailResponse -import org.yapp.infra.external.aladin.response.AladinSearchResponse - -@Service -@Qualifier(BookQueryServiceQualifier.ALADIN) -class AladinBookQueryService( - private val aladinApiHelper: AladinApiHelper, -) : BookQueryService { - private val log = KotlinLogging.logger {} - - override fun searchBooks(request: BookSearchRequest): BookSearchResponse { - log.info("Service - Converting BookSearchRequest to AladinBookSearchRequest and calling Aladin API for book search.") - val aladinSearchRequest = request.toAladinRequest() - val response: AladinSearchResponse = aladinApiHelper.searchBooks(aladinSearchRequest) - return BookSearchResponse.Companion.from(response) - } - - - override fun getBookDetail(request: BookDetailRequest): BookDetailResponse { - log.info("Service - Converting BookDetailRequest to AladinBookLookupRequest and calling Aladin API for book detail lookup.") - val aladinLookupRequest = request.toAladinRequest() - val aladinResponse: AladinBookDetailResponse = aladinApiHelper.lookupBook(aladinLookupRequest) - return BookDetailResponse.Companion.from(aladinResponse) - } -} diff --git a/apis/src/main/kotlin/org/yapp/apis/book/usecase/BookUseCase.kt b/apis/src/main/kotlin/org/yapp/apis/book/usecase/BookUseCase.kt index 2d273923..7569f2bc 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/usecase/BookUseCase.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/usecase/BookUseCase.kt @@ -1,70 +1,75 @@ package org.yapp.apis.book.usecase + import org.springframework.beans.factory.annotation.Qualifier -import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.transaction.annotation.Transactional import org.yapp.apis.auth.dto.request.UserBooksByIsbnsRequest import org.yapp.apis.auth.service.UserAuthService -import org.yapp.apis.book.dto.request.BookCreateRequest -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.request.* 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.constant.BookQueryServiceQualifier -import org.yapp.apis.book.dto.request.UpsertUserBookRequest import org.yapp.apis.book.service.BookManagementService import org.yapp.apis.book.service.BookQueryService import org.yapp.apis.book.service.UserBookService import org.yapp.domain.userbook.BookStatus import org.yapp.domain.userbook.UserBookSortType import org.yapp.globalutils.annotation.UseCase -import java.util.UUID +import java.util.* @UseCase @Transactional(readOnly = true) class BookUseCase( - @Qualifier(BookQueryServiceQualifier.ALADIN) + @Qualifier("aladinBookQueryService") private val bookQueryService: BookQueryService, private val userAuthService: UserAuthService, private val userBookService: UserBookService, private val bookManagementService: BookManagementService ) { - fun searchBooks(request: BookSearchRequest, userId: UUID): BookSearchResponse { + fun searchBooks( + request: BookSearchRequest, + userId: UUID + ): BookSearchResponse { userAuthService.validateUserExists(userId) val searchResponse = bookQueryService.searchBooks(request) - val isbns = searchResponse.books.map { it.isbn } + val booksWithUserStatus = mergeWithUserBookStatus(searchResponse.books, userId) - val userBooksReponse = userBookService.findAllByUserIdAndBookIsbnIn(UserBooksByIsbnsRequest.of(userId, isbns)) - val statusMap = userBooksReponse.associateBy({ it.bookIsbn }, { it.status }) + return searchResponse.withUpdatedBooks(booksWithUserStatus) + } - val updatedBooks = searchResponse.books.map { bookSummary -> - statusMap[bookSummary.isbn]?.let { status -> - bookSummary.updateStatus(status) - } ?: bookSummary - } + fun getBookDetail( + bookDetailRequest: BookDetailRequest, + userId: UUID + ): BookDetailResponse { + userAuthService.validateUserExists(userId) - return searchResponse.from(updatedBooks) - } + val bookDetailResponse = bookQueryService.getBookDetail(bookDetailRequest) + val isbn13 = bookDetailResponse.isbn13 + ?: return bookDetailResponse.withUserBookStatus(BookStatus.BEFORE_REGISTRATION) + + val userBookStatus = userBookService.findUserBookStatusByIsbn(userId, isbn13) + ?: BookStatus.BEFORE_REGISTRATION - fun getBookDetail(bookDetailRequest: BookDetailRequest): BookDetailResponse { - return bookQueryService.getBookDetail(bookDetailRequest) + return bookDetailResponse.withUserBookStatus(userBookStatus) } @Transactional - fun upsertBookToMyLibrary(userId: UUID, request: UserBookRegisterRequest): UserBookResponse { + fun upsertBookToMyLibrary( + userId: UUID, + request: UserBookRegisterRequest + ): UserBookResponse { userAuthService.validateUserExists(userId) - val bookDetailResponse = bookQueryService.getBookDetail(BookDetailRequest.of(request.validBookIsbn())) + val bookDetailResponse = bookQueryService.getBookDetail(BookDetailRequest.from(request.validBookIsbn())) val bookCreateResponse = bookManagementService.findOrCreateBook(BookCreateRequest.from(bookDetailResponse)) val upsertUserBookRequest = UpsertUserBookRequest.of( userId = userId, bookCreateResponse, - status = request.bookStatus, + status = request.validBookStatus(), ) val userBookResponse = userBookService.upsertUserBook(upsertUserBookRequest) @@ -82,4 +87,38 @@ class BookUseCase( return userBookService.findUserBooksByDynamicConditionWithStatusCounts(userId, status, sort, title, pageable) } + + private fun mergeWithUserBookStatus( + searchedBooks: List, + userId: UUID + ): List { + if (searchedBooks.isEmpty()) { + return emptyList() + } + + val isbn13List = searchedBooks.map { it.isbn13 } + val userBookStatusMap = getUserBookStatusMap(isbn13List, userId) + + return searchedBooks.map { bookSummary -> + val userStatus = userBookStatusMap[bookSummary.isbn13] + if (userStatus != null) { + bookSummary.updateStatus(userStatus) + } else { + bookSummary + } + } + } + + private fun getUserBookStatusMap( + isbn13List: List, + userId: UUID + ): Map { + val userBooksResponse = userBookService.findAllByUserIdAndBookIsbnIn( + UserBooksByIsbnsRequest.of(userId, isbn13List) + ) + + return userBooksResponse.associate { userBook -> + userBook.bookIsbn to userBook.status + } + } } diff --git a/apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequest.kt b/apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequest.kt index 3d441264..7cb7a658 100644 --- a/apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequest.kt @@ -6,9 +6,19 @@ import jakarta.validation.constraints.Min import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.Size - -@Schema(description = "독서 기록 생성 요청") +@Schema( + description = "독서 기록 생성 요청", + example = """ + { + "pageNumber": 42, + "quote": "이것은 기억에 남는 문장입니다.", + "review": "이 책은 매우 인상적이었습니다.", + "emotionTags": ["감동적"] + } + """ +) data class CreateReadingRecordRequest private constructor( + @field:Min(1, message = "페이지 번호는 1 이상이어야 합니다.") @field:Max(9999, message = "페이지 번호는 9999 이하여야 합니다.") @Schema(description = "현재 읽은 페이지 번호", example = "42", required = true) @@ -24,8 +34,8 @@ data class CreateReadingRecordRequest private constructor( @Schema(description = "감상평", example = "이 책은 매우 인상적이었습니다.", required = true) val review: String? = null, - @field:Size(max = 3, message = "감정 태그는 최대 3개까지 가능합니다.") - @Schema(description = "감정 태그 목록 (최대 3개)", example = "[\"감동적\", \"슬픔\", \"희망\"]") + @field:Size(max = 1, message = "감정 태그는 최대 1개까지 가능합니다. (단일 감정만 받지만, 확장성을 위해 리스트 형태로 관리됩니다.)") + @Schema(description = "감정 태그 목록 (현재는 최대 1개, 확장 가능)", example = "[\"감동적\"]") val emotionTags: List<@Size(max = 10, message = "감정 태그는 10자를 초과할 수 없습니다.") String> = emptyList() ) { fun validPageNumber(): Int = pageNumber!! diff --git a/apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordService.kt b/apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordService.kt index f015fd54..4dd2d3d0 100644 --- a/apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordService.kt @@ -1,23 +1,21 @@ package org.yapp.apis.readingrecord.service +import jakarta.validation.Valid import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.stereotype.Service -import org.yapp.apis.book.service.UserBookService +import org.springframework.validation.annotation.Validated import org.yapp.apis.readingrecord.dto.request.CreateReadingRecordRequest import org.yapp.apis.readingrecord.dto.response.ReadingRecordResponse -import org.yapp.apis.readingrecord.exception.ReadingRecordErrorCode -import org.yapp.apis.readingrecord.exception.ReadingRecordNotFoundException -import org.yapp.domain.book.BookDomainService import org.yapp.domain.readingrecord.ReadingRecordDomainService import org.yapp.domain.readingrecord.ReadingRecordSortType import java.util.* @Service +@Validated class ReadingRecordService( private val readingRecordDomainService: ReadingRecordDomainService, - private val userBookService: UserBookService, ) { fun createReadingRecord( @@ -25,8 +23,6 @@ class ReadingRecordService( userBookId: UUID, request: CreateReadingRecordRequest ): ReadingRecordResponse { - userBookService.validateUserBookExists(userId, userBookId) - val readingRecordInfoVO = readingRecordDomainService.createReadingRecord( userBookId = userBookId, pageNumber = request.validPageNumber(), @@ -43,13 +39,6 @@ class ReadingRecordService( readingRecordId: UUID ): ReadingRecordResponse { val readingRecordInfoVO = readingRecordDomainService.findReadingRecordById(readingRecordId) - ?: throw ReadingRecordNotFoundException( - ReadingRecordErrorCode.READING_RECORD_NOT_FOUND, - "Reading record not found with id: $readingRecordId" - ) - - userBookService.validateUserBookExists(userId, readingRecordInfoVO.userBookId.value) - return ReadingRecordResponse.from(readingRecordInfoVO) } diff --git a/apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCase.kt b/apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCase.kt index 99271b5b..477cfafa 100644 --- a/apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCase.kt +++ b/apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCase.kt @@ -26,7 +26,7 @@ class ReadingRecordUseCase( request: CreateReadingRecordRequest ): ReadingRecordResponse { userAuthService.validateUserExists(userId) - userBookService.validateUserBookExists(userId, userBookId) + userBookService.validateUserBookExists(userBookId, userId) return readingRecordService.createReadingRecord( userId = userId, @@ -54,8 +54,7 @@ class ReadingRecordUseCase( pageable: Pageable ): Page { userAuthService.validateUserExists(userId) - - userBookService.validateUserBookExists(userId, userBookId) + userBookService.validateUserBookExists(userBookId, userId) return readingRecordService.getReadingRecordsByDynamicCondition( userBookId = userBookId, diff --git a/apis/src/main/kotlin/org/yapp/apis/util/AuthorExtractor.kt b/apis/src/main/kotlin/org/yapp/apis/util/AuthorExtractor.kt new file mode 100644 index 00000000..85819fa5 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/util/AuthorExtractor.kt @@ -0,0 +1,21 @@ +package org.yapp.apis.util + +object AuthorExtractor { + private const val AUTHOR_MARKER = "(지은이)" + private const val CLOSING_PAREN = ")" + private const val DELIMITER = "), " + + fun extractAuthors(authorString: String?): String { + if (authorString.isNullOrBlank() || !authorString.contains(AUTHOR_MARKER)) { + return "" + } + + var authorsPart = authorString.substringBefore(" $AUTHOR_MARKER") + + if (authorsPart.contains(CLOSING_PAREN)) { + authorsPart = authorsPart.substringAfterLast(DELIMITER) + } + + return authorsPart.trim() + } +} diff --git a/apis/src/main/kotlin/org/yapp/apis/util/IsbnConverter.kt b/apis/src/main/kotlin/org/yapp/apis/util/IsbnConverter.kt new file mode 100644 index 00000000..caa5ba26 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/util/IsbnConverter.kt @@ -0,0 +1,33 @@ +package org.yapp.apis.util + +import org.yapp.globalutils.util.RegexUtils + +object IsbnConverter { + private const val ISBN10_LENGTH = 10 + private const val ISBN13_PREFIX = "978" + private const val ISBN13_MODULUS = 10 + private const val WEIGHT_EVEN = 3 + private const val WEIGHT_ODD = 1 + + fun toIsbn13(isbn10: String?): String? { + val sanitized = isbn10?.replace("-", "")?.uppercase() + + if (sanitized.isNullOrBlank() || sanitized.length != ISBN10_LENGTH) { + return null + } + if (!sanitized.matches(Regex(RegexUtils.ISBN10_PATTERN))) { + return null + } + + val stem = ISBN13_PREFIX + sanitized.substring(0, ISBN10_LENGTH - 1) + + val sum = stem.mapIndexed { index, c -> + val digit = c.digitToInt() + if ((index + 1) % 2 == 0) digit * WEIGHT_EVEN else digit * WEIGHT_ODD + }.sum() + + val checkDigit = (ISBN13_MODULUS - (sum % ISBN13_MODULUS)) % ISBN13_MODULUS + + return stem + checkDigit + } +} diff --git a/apis/src/main/kotlin/org/yapp/apis/util/generateNickname.kt b/apis/src/main/kotlin/org/yapp/apis/util/NicknameGenerator.kt similarity index 100% rename from apis/src/main/kotlin/org/yapp/apis/util/generateNickname.kt rename to apis/src/main/kotlin/org/yapp/apis/util/NicknameGenerator.kt diff --git a/apis/src/main/resources/application.yml b/apis/src/main/resources/application.yml index afda8bd2..aaf88d7c 100644 --- a/apis/src/main/resources/application.yml +++ b/apis/src/main/resources/application.yml @@ -25,7 +25,7 @@ spring: springdoc: swagger-ui: - path: /swagger-ui.html + path: /swagger-ui enabled: true api-docs: path: /v3/api-docs diff --git a/domain/src/main/kotlin/org/yapp/domain/book/Book.kt b/domain/src/main/kotlin/org/yapp/domain/book/Book.kt index a86022c0..d9286495 100644 --- a/domain/src/main/kotlin/org/yapp/domain/book/Book.kt +++ b/domain/src/main/kotlin/org/yapp/domain/book/Book.kt @@ -3,11 +3,8 @@ package org.yapp.domain.book import org.yapp.globalutils.util.UuidGenerator import org.yapp.globalutils.validator.IsbnValidator import java.time.LocalDateTime -import java.util.UUID +import java.util.* -/** - * Represents a book in the domain model. - */ data class Book private constructor( val id: Id, val isbn: Isbn, diff --git a/domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt b/domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt index fedac437..bdf772a8 100644 --- a/domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt +++ b/domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt @@ -2,6 +2,8 @@ package org.yapp.domain.readingrecord import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable +import org.yapp.domain.readingrecord.exception.ReadingRecordErrorCode +import org.yapp.domain.readingrecord.exception.ReadingRecordNotFoundException import org.yapp.domain.readingrecord.vo.ReadingRecordInfoVO import org.yapp.domain.readingrecordtag.ReadingRecordTag import org.yapp.domain.readingrecordtag.ReadingRecordTagRepository @@ -59,8 +61,12 @@ class ReadingRecordDomainService( ) } - fun findReadingRecordById(readingRecordId: UUID): ReadingRecordInfoVO? { - val readingRecord = readingRecordRepository.findById(readingRecordId) ?: return null + fun findReadingRecordById(readingRecordId: UUID): ReadingRecordInfoVO { + val readingRecord = readingRecordRepository.findById(readingRecordId) + ?: throw ReadingRecordNotFoundException( + ReadingRecordErrorCode.READING_RECORD_NOT_FOUND, + "Reading record not found with id: $readingRecordId" + ) return buildReadingRecordInfoVO(readingRecord) } diff --git a/domain/src/main/kotlin/org/yapp/domain/readingrecord/exception/ReadingRecordErrorCode.kt b/domain/src/main/kotlin/org/yapp/domain/readingrecord/exception/ReadingRecordErrorCode.kt new file mode 100644 index 00000000..9ec94af4 --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/readingrecord/exception/ReadingRecordErrorCode.kt @@ -0,0 +1,16 @@ +package org.yapp.domain.readingrecord.exception + +import org.springframework.http.HttpStatus +import org.yapp.globalutils.exception.BaseErrorCode + +enum class ReadingRecordErrorCode( + private val status: HttpStatus, + private val code: String, + private val message: String +) : BaseErrorCode { + READING_RECORD_NOT_FOUND(HttpStatus.NOT_FOUND, "READING_RECORD_001", "독서 기록을 찾을 수 없습니다."); + + override fun getHttpStatus(): HttpStatus = status + override fun getCode(): String = code + override fun getMessage(): String = message +} diff --git a/apis/src/main/kotlin/org/yapp/apis/readingrecord/exception/ReadingRecordNotFoundException.kt b/domain/src/main/kotlin/org/yapp/domain/readingrecord/exception/ReadingRecordNotFoundException.kt similarity index 80% rename from apis/src/main/kotlin/org/yapp/apis/readingrecord/exception/ReadingRecordNotFoundException.kt rename to domain/src/main/kotlin/org/yapp/domain/readingrecord/exception/ReadingRecordNotFoundException.kt index 91f1f359..fdf1c580 100644 --- a/apis/src/main/kotlin/org/yapp/apis/readingrecord/exception/ReadingRecordNotFoundException.kt +++ b/domain/src/main/kotlin/org/yapp/domain/readingrecord/exception/ReadingRecordNotFoundException.kt @@ -1,4 +1,4 @@ -package org.yapp.apis.readingrecord.exception +package org.yapp.domain.readingrecord.exception import org.yapp.globalutils.exception.CommonException diff --git a/domain/src/main/kotlin/org/yapp/domain/userbook/UserBookDomainService.kt b/domain/src/main/kotlin/org/yapp/domain/userbook/UserBookDomainService.kt index 2928196b..445ec5f4 100644 --- a/domain/src/main/kotlin/org/yapp/domain/userbook/UserBookDomainService.kt +++ b/domain/src/main/kotlin/org/yapp/domain/userbook/UserBookDomainService.kt @@ -57,6 +57,11 @@ class UserBookDomainService( return userBooks.map { UserBookInfoVO.newInstance(it) } } + fun findByUserIdAndBookIsbn(userId: UUID, isbn: String): UserBookInfoVO? { + val userBook = userBookRepository.findByUserIdAndBookIsbn(userId, isbn) + return userBook?.let { UserBookInfoVO.newInstance(it) } + } + fun getUserBookStatusCounts(userId: UUID): UserBookStatusCountsVO { val statusCounts = BookStatus.entries.associateWith { status -> countUserBooksByStatus(userId, status) @@ -68,8 +73,8 @@ class UserBookDomainService( return userBookRepository.countUserBooksByStatus(userId, status) } - fun findByIdAndUserId(userBookId: UUID, userId: UUID): UserBook? { - return userBookRepository.findByIdAndUserId(userBookId, userId) + fun existsByUserBookIdAndUserId(userBookId: UUID, userId: UUID): Boolean { + return userBookRepository.existsByIdAndUserId(userBookId, userId) } fun findBooksWithRecordsOrderByLatest(userId: UUID): List { @@ -103,4 +108,4 @@ class UserBookDomainService( ) } } -} +} \ No newline at end of file diff --git a/domain/src/main/kotlin/org/yapp/domain/userbook/UserBookRepository.kt b/domain/src/main/kotlin/org/yapp/domain/userbook/UserBookRepository.kt index 56b2a9ac..4b2c23ba 100644 --- a/domain/src/main/kotlin/org/yapp/domain/userbook/UserBookRepository.kt +++ b/domain/src/main/kotlin/org/yapp/domain/userbook/UserBookRepository.kt @@ -10,7 +10,7 @@ interface UserBookRepository { fun findByUserIdAndBookIsbn(userId: UUID, isbn: String): UserBook? fun findByBookIdAndUserId(bookId: UUID, userId: UUID): UserBook? - fun findByIdAndUserId(id: UUID, userId: UUID): UserBook? + fun existsByIdAndUserId(id: UUID, userId: UUID): Boolean fun findById(id: UUID): UserBook? fun save(userBook: UserBook): UserBook diff --git a/domain/src/main/kotlin/org/yapp/domain/userbook/exception/UserBookErrorCode.kt b/domain/src/main/kotlin/org/yapp/domain/userbook/exception/UserBookErrorCode.kt new file mode 100644 index 00000000..bfadebdd --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/userbook/exception/UserBookErrorCode.kt @@ -0,0 +1,16 @@ +package org.yapp.domain.userbook.exception + +import org.springframework.http.HttpStatus +import org.yapp.globalutils.exception.BaseErrorCode + +enum class UserBookErrorCode( + private val status: HttpStatus, + private val code: String, + private val message: String +) : BaseErrorCode { + USER_BOOK_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_BOOK_001", "유저의 책 정보를 찾을 수 없습니다."); + + override fun getHttpStatus(): HttpStatus = status + override fun getCode(): String = code + override fun getMessage(): String = message +} diff --git a/domain/src/main/kotlin/org/yapp/domain/userbook/exception/UserBookNotFoundException.kt b/domain/src/main/kotlin/org/yapp/domain/userbook/exception/UserBookNotFoundException.kt new file mode 100644 index 00000000..b6680af0 --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/userbook/exception/UserBookNotFoundException.kt @@ -0,0 +1,8 @@ +package org.yapp.domain.userbook.exception + +import org.yapp.globalutils.exception.CommonException + +class UserBookNotFoundException ( + errorCode: UserBookErrorCode, + message: String? = null +) : CommonException(errorCode, message) diff --git a/global-utils/src/main/kotlin/org/yapp/globalutils/book/BookCoverSize.kt b/global-utils/src/main/kotlin/org/yapp/globalutils/book/BookCoverSize.kt new file mode 100644 index 00000000..0cbbc2f6 --- /dev/null +++ b/global-utils/src/main/kotlin/org/yapp/globalutils/book/BookCoverSize.kt @@ -0,0 +1,10 @@ +package org.yapp.globalutils.book + +enum class BookCoverSize(val apiValue: String) { + BIG("Big"), + MID_BIG("MidBig"), + MID("Mid"), + SMALL("Small"), + MINI("Mini"), + NONE("None") +} diff --git a/global-utils/src/main/kotlin/org/yapp/globalutils/util/RegexUtils.kt b/global-utils/src/main/kotlin/org/yapp/globalutils/util/RegexUtils.kt index eec3afda..ff1810f8 100644 --- a/global-utils/src/main/kotlin/org/yapp/globalutils/util/RegexUtils.kt +++ b/global-utils/src/main/kotlin/org/yapp/globalutils/util/RegexUtils.kt @@ -1,18 +1,6 @@ package org.yapp.globalutils.util object RegexUtils { - - val EMAIL_PATTERN = Regex("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$") - - val PROFILE_IMAGE_URL_PATTERN = Regex("^https?://[a-zA-Z0-9.-]+(/.*)?$") - - const val NOT_BLANK_AND_NOT_NULL_STRING_PATTERN = "^(?!null$|NULL$|\\s*$).+" // Removed the old ISBN pattern - - fun isValidEmail(email: String): Boolean { - return email.matches(EMAIL_PATTERN) - } - - fun isValidProfileImageUrl(url: String): Boolean { - return url.matches(PROFILE_IMAGE_URL_PATTERN) - } + const val ISBN10_PATTERN = "^[0-9]{9}[0-9X]$" + const val ISBN13_PATTERN = "^(978|979)\\d{10}$" } diff --git a/infra/src/main/kotlin/org/yapp/infra/external/aladin/AladinApi.kt b/infra/src/main/kotlin/org/yapp/infra/external/aladin/AladinApi.kt index 1680564d..f58fe191 100644 --- a/infra/src/main/kotlin/org/yapp/infra/external/aladin/AladinApi.kt +++ b/infra/src/main/kotlin/org/yapp/infra/external/aladin/AladinApi.kt @@ -2,8 +2,8 @@ package org.yapp.infra.external.aladin import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component -import org.yapp.infra.external.aladin.dto.AladinBookLookupRequest -import org.yapp.infra.external.aladin.dto.AladinBookSearchRequest +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.AladinSearchResponse @@ -16,15 +16,14 @@ class AladinApi( fun searchBooks(request: AladinBookSearchRequest): Result { return runCatching { val aladinApiParams = request.toMap() - aladinRestClient.itemSearch(ttbKey, aladinApiParams) // Map으로 전달 + aladinRestClient.itemSearch(ttbKey, aladinApiParams) } } fun lookupBook(request: AladinBookLookupRequest): Result { return runCatching { - val aladinApiParams = request.toMap().toMutableMap() - aladinApiParams["Cover"] = "Big" - aladinRestClient.itemLookUp(ttbKey, aladinApiParams) // Map으로 전달 + val aladinApiParams = request.toMap() + aladinRestClient.itemLookUp(ttbKey, aladinApiParams) } } } diff --git a/infra/src/main/kotlin/org/yapp/infra/external/aladin/AladinRestClient.kt b/infra/src/main/kotlin/org/yapp/infra/external/aladin/AladinRestClient.kt index e8e858f6..ffb93338 100644 --- a/infra/src/main/kotlin/org/yapp/infra/external/aladin/AladinRestClient.kt +++ b/infra/src/main/kotlin/org/yapp/infra/external/aladin/AladinRestClient.kt @@ -9,26 +9,12 @@ import org.yapp.infra.external.aladin.response.AladinSearchResponse @Component class AladinRestClient( - @Qualifier("aladinApiRestClient") private val restClient: RestClient + @Qualifier("aladinApiRestClient") + private val restClient: RestClient ) { - - private val client = restClient - private val API_VERSION = "20131101" private val DEFAULT_OUTPUT_FORMAT = "JS" - private fun UriComponentsBuilder.addCommonQueryParams(params: Map) { - params.forEach { (key, value) -> - if (key == "OptResult" && value is List<*>) { - this.queryParam(key, value.joinToString(",")) - } else { - this.queryParam(key, value) - } - } - this.queryParam("output", DEFAULT_OUTPUT_FORMAT) - .queryParam("Version", API_VERSION) - } - fun itemSearch( ttbKey: String?, params: Map @@ -38,7 +24,7 @@ class AladinRestClient( uriBuilder.addCommonQueryParams(params) - return client.get() + return restClient.get() .uri(uriBuilder.build().toUriString()) .retrieve() .body(AladinSearchResponse::class.java) @@ -54,10 +40,22 @@ class AladinRestClient( uriBuilder.addCommonQueryParams(params) - return client.get() + return restClient.get() .uri(uriBuilder.build().toUriString()) .retrieve() .body(AladinBookDetailResponse::class.java) ?: throw IllegalStateException("Aladin ItemLookUp API 응답이 null 입니다.") } + + private fun UriComponentsBuilder.addCommonQueryParams(params: Map) { + params.forEach { (key, value) -> + if (key == "OptResult" && value is List<*>) { + this.queryParam(key, value.joinToString(",")) + } else { + this.queryParam(key, value) + } + } + this.queryParam("Output", DEFAULT_OUTPUT_FORMAT) + .queryParam("Version", API_VERSION) + } } diff --git a/infra/src/main/kotlin/org/yapp/infra/external/aladin/request/AladinBookLookupRequest.kt b/infra/src/main/kotlin/org/yapp/infra/external/aladin/request/AladinBookLookupRequest.kt index 1c5d2052..a0575fc2 100644 --- a/infra/src/main/kotlin/org/yapp/infra/external/aladin/request/AladinBookLookupRequest.kt +++ b/infra/src/main/kotlin/org/yapp/infra/external/aladin/request/AladinBookLookupRequest.kt @@ -1,33 +1,29 @@ -package org.yapp.infra.external.aladin.dto +package org.yapp.infra.external.aladin.request -/** - * 알라딘 ItemLookUp API 호출을 위한 요청 DTO. - * 내부적으로 알라딘 API 파라미터 규칙에 맞게 변환하는 책임을 가집니다. - */ -data class AladinBookLookupRequest private constructor( // private constructor 유지 +import org.yapp.globalutils.book.BookCoverSize + +data class AladinBookLookupRequest private constructor( val itemId: String, - val itemIdType: String, - val optResult: List? + val itemIdType: String = "ISBN13", + val cover: String? ) { fun toMap(): Map { val params = mutableMapOf() params["ItemId"] = itemId params["ItemIdType"] = itemIdType - optResult?.let { - if (it.isNotEmpty()) { - params["OptResult"] = it - } - } + cover?.let { params["Cover"] = it } return params } companion object { - fun create( + fun from( itemId: String, - itemIdType: String, - optResult: List? = null ): AladinBookLookupRequest { - return AladinBookLookupRequest(itemId, itemIdType, optResult) + return AladinBookLookupRequest( + itemId = itemId, + itemIdType = "ISBN13", + cover = BookCoverSize.BIG.apiValue + ) } } } diff --git a/infra/src/main/kotlin/org/yapp/infra/external/aladin/request/AladinBookSearchRequest.kt b/infra/src/main/kotlin/org/yapp/infra/external/aladin/request/AladinBookSearchRequest.kt index 32714ebc..bbd5d70f 100644 --- a/infra/src/main/kotlin/org/yapp/infra/external/aladin/request/AladinBookSearchRequest.kt +++ b/infra/src/main/kotlin/org/yapp/infra/external/aladin/request/AladinBookSearchRequest.kt @@ -1,4 +1,6 @@ -package org.yapp.infra.external.aladin.dto +package org.yapp.infra.external.aladin.request + +import org.yapp.globalutils.book.BookCoverSize data class AladinBookSearchRequest private constructor( val query: String, @@ -8,7 +10,7 @@ data class AladinBookSearchRequest private constructor( val start: Int?, val sort: String?, val cover: String?, - val categoryId: Int? + val categoryId: Int?, ) { fun toMap(): Map { val params = mutableMapOf() @@ -24,14 +26,13 @@ data class AladinBookSearchRequest private constructor( } companion object { - fun create( + fun of( query: String, queryType: String? = null, searchTarget: String? = null, maxResults: Int? = null, start: Int? = null, sort: String? = null, - cover: String? = null, categoryId: Int? = null ): AladinBookSearchRequest { return AladinBookSearchRequest( @@ -41,8 +42,8 @@ data class AladinBookSearchRequest private constructor( maxResults = maxResults, start = start, sort = sort, - cover = cover, - categoryId = categoryId + cover = BookCoverSize.BIG.apiValue, + categoryId = categoryId, ) } } diff --git a/infra/src/main/kotlin/org/yapp/infra/external/aladin/response/AladinDetailResponse.kt b/infra/src/main/kotlin/org/yapp/infra/external/aladin/response/AladinDetailResponse.kt new file mode 100644 index 00000000..404ec5b8 --- /dev/null +++ b/infra/src/main/kotlin/org/yapp/infra/external/aladin/response/AladinDetailResponse.kt @@ -0,0 +1,52 @@ +package org.yapp.infra.external.aladin.response + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty +import java.math.BigDecimal + +@JsonIgnoreProperties(ignoreUnknown = true) +data class AladinBookDetailResponse internal constructor( + @JsonProperty("version") val version: String?, + @JsonProperty("title") val title: String?, + @JsonProperty("link") val link: String?, + @JsonProperty("pubDate") val pubDate: String?, + @JsonProperty("totalResults") val totalResults: Int?, + @JsonProperty("startIndex") val startIndex: Int?, + @JsonProperty("itemsPerPage") val itemsPerPage: Int?, + @JsonProperty("query") val query: String? = null, + @JsonProperty("searchCategoryId") val searchCategoryId: Int? = null, + @JsonProperty("searchCategoryName") val searchCategoryName: String? = null, + @JsonProperty("item") val item: List = emptyList() +) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class AladinDetailItem internal constructor( + @JsonProperty("title") val title: String, + @JsonProperty("link") val link: String, + @JsonProperty("author") val author: String?, + @JsonProperty("pubDate") val pubDate: String?, + @JsonProperty("description") val description: String?, + @JsonProperty("isbn") val isbn: String?, + @JsonProperty("isbn13") val isbn13: String?, + @JsonProperty("itemId") val itemId: Long, + @JsonProperty("priceSales") val priceSales: BigDecimal, + @JsonProperty("priceStandard") val priceStandard: BigDecimal, + @JsonProperty("mallType") val mallType: String, + @JsonProperty("stockStatus") val stockStatus: String?, + @JsonProperty("mileage") val mileage: Int?, + @JsonProperty("cover") val cover: String, + @JsonProperty("categoryId") val categoryId: Int, + @JsonProperty("categoryName") val categoryName: String, + @JsonProperty("publisher") val publisher: String?, + @JsonProperty("subInfo") val subInfo: AladinDetailSubInfo +) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class AladinDetailSubInfo( + @JsonProperty("subTitle") + val subTitle: String?, + @JsonProperty("originalTitle") + val originalTitle: String?, + @JsonProperty("itemPage") + val itemPage: Int? +) diff --git a/infra/src/main/kotlin/org/yapp/infra/external/aladin/response/AladinResponseBase.kt b/infra/src/main/kotlin/org/yapp/infra/external/aladin/response/AladinResponseBase.kt deleted file mode 100644 index 56e7975c..00000000 --- a/infra/src/main/kotlin/org/yapp/infra/external/aladin/response/AladinResponseBase.kt +++ /dev/null @@ -1,53 +0,0 @@ -package org.yapp.infra.external.aladin.response - -import com.fasterxml.jackson.annotation.JsonProperty -import java.math.BigDecimal - -data class BookItem internal constructor( - @JsonProperty("title") val title: String?, - @JsonProperty("link") val link: String?, - @JsonProperty("author") val author: String?, - @JsonProperty("pubDate") val pubDate: String?, - @JsonProperty("description") val description: String?, - @JsonProperty("isbn") val isbn: String?, - @JsonProperty("isbn13") val isbn13: String?, - @JsonProperty("itemId") val itemId: Long?, - @JsonProperty("priceSales") val priceSales: BigDecimal?, - @JsonProperty("priceStandard") val priceStandard: BigDecimal?, - @JsonProperty("mallType") val mallType: String?, - @JsonProperty("stockStatus") val stockStatus: String? = null, - @JsonProperty("mileage") val mileage: Int?, - @JsonProperty("cover") val cover: String?, - @JsonProperty("categoryId") val categoryId: Int?, - @JsonProperty("categoryName") val categoryName: String?, - @JsonProperty("publisher") val publisher: String?, - @JsonProperty("itemPage") val itemPage: Int? -) - -data class AladinSearchResponse internal constructor( - @JsonProperty("version") val version: String?, - @JsonProperty("title") val title: String?, - @JsonProperty("link") val link: String?, - @JsonProperty("pubDate") val pubDate: String?, - @JsonProperty("totalResults") val totalResults: Int?, - @JsonProperty("startIndex") val startIndex: Int?, - @JsonProperty("itemsPerPage") val itemsPerPage: Int?, - @JsonProperty("query") val query: String? = null, - @JsonProperty("searchCategoryId") val searchCategoryId: Int? = null, - @JsonProperty("searchCategoryName") val searchCategoryName: String? = null, - @JsonProperty("item") val item: List? = null -) - -data class AladinBookDetailResponse( - @JsonProperty("version") val version: String?, - @JsonProperty("title") val title: String?, - @JsonProperty("link") val link: String?, - @JsonProperty("pubDate") val pubDate: String?, - @JsonProperty("totalResults") val totalResults: Int?, - @JsonProperty("startIndex") val startIndex: Int?, - @JsonProperty("itemsPerPage") val itemsPerPage: Int?, - @JsonProperty("query") val query: String? = null, - @JsonProperty("searchCategoryId") val searchCategoryId: Int? = null, - @JsonProperty("searchCategoryName") val searchCategoryName: String? = null, - @JsonProperty("item") val item: List? = null -) diff --git a/infra/src/main/kotlin/org/yapp/infra/external/aladin/response/AladinSearchResponse.kt b/infra/src/main/kotlin/org/yapp/infra/external/aladin/response/AladinSearchResponse.kt new file mode 100644 index 00000000..925d631c --- /dev/null +++ b/infra/src/main/kotlin/org/yapp/infra/external/aladin/response/AladinSearchResponse.kt @@ -0,0 +1,41 @@ +package org.yapp.infra.external.aladin.response + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty +import java.math.BigDecimal + +@JsonIgnoreProperties(ignoreUnknown = true) +data class AladinSearchResponse internal constructor( + @JsonProperty("version") val version: String?, + @JsonProperty("title") val title: String?, + @JsonProperty("link") val link: String?, + @JsonProperty("pubDate") val pubDate: String?, + @JsonProperty("totalResults") val totalResults: Int?, + @JsonProperty("startIndex") val startIndex: Int?, + @JsonProperty("itemsPerPage") val itemsPerPage: Int?, + @JsonProperty("query") val query: String? = null, + @JsonProperty("searchCategoryId") val searchCategoryId: Int? = null, + @JsonProperty("searchCategoryName") val searchCategoryName: String? = null, + @JsonProperty("item") val item: List = emptyList() +) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class AladinSearchItem internal constructor( + @JsonProperty("title") val title: String, + @JsonProperty("link") val link: String, + @JsonProperty("author") val author: String?, + @JsonProperty("pubDate") val pubDate: String?, + @JsonProperty("description") val description: String?, + @JsonProperty("isbn") val isbn: String?, + @JsonProperty("isbn13") val isbn13: String?, + @JsonProperty("itemId") val itemId: Long, + @JsonProperty("priceSales") val priceSales: BigDecimal, + @JsonProperty("priceStandard") val priceStandard: BigDecimal, + @JsonProperty("mallType") val mallType: String, + @JsonProperty("stockStatus") val stockStatus: String?, + @JsonProperty("mileage") val mileage: Int?, + @JsonProperty("cover") val cover: String, + @JsonProperty("categoryId") val categoryId: Int, + @JsonProperty("categoryName") val categoryName: String, + @JsonProperty("publisher") val publisher: String? +) diff --git a/infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/ReadingRecordRepositoryImpl.kt b/infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/ReadingRecordRepositoryImpl.kt index fe87c118..03663a40 100644 --- a/infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/ReadingRecordRepositoryImpl.kt +++ b/infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/ReadingRecordRepositoryImpl.kt @@ -7,10 +7,9 @@ import org.springframework.stereotype.Repository import org.yapp.domain.readingrecord.ReadingRecord import org.yapp.domain.readingrecord.ReadingRecordRepository import org.yapp.domain.readingrecord.ReadingRecordSortType -import org.yapp.domain.userbook.UserBookSortType import org.yapp.infra.readingrecord.entity.ReadingRecordEntity import org.yapp.infra.readingrecord.repository.JpaReadingRecordRepository -import java.util.UUID +import java.util.* @Repository class ReadingRecordRepositoryImpl( diff --git a/infra/src/main/kotlin/org/yapp/infra/userbook/repository/JpaUserBookRepository.kt b/infra/src/main/kotlin/org/yapp/infra/userbook/repository/JpaUserBookRepository.kt index d6887977..c63fee6b 100644 --- a/infra/src/main/kotlin/org/yapp/infra/userbook/repository/JpaUserBookRepository.kt +++ b/infra/src/main/kotlin/org/yapp/infra/userbook/repository/JpaUserBookRepository.kt @@ -7,7 +7,7 @@ import java.util.* interface JpaUserBookRepository : JpaRepository, JpaUserBookQuerydslRepository { fun findByUserIdAndBookIsbn(userId: UUID, bookIsbn: String): UserBookEntity? fun findByBookIdAndUserId(bookId: UUID, userId: UUID): UserBookEntity? - fun findByIdAndUserId(id: UUID, userId: UUID): UserBookEntity? + fun existsByIdAndUserId(id: UUID, userId: UUID): Boolean fun findAllByUserId(userId: UUID): List fun findAllByUserIdAndBookIsbnIn(userId: UUID, bookIsbnList: List): List } diff --git a/infra/src/main/kotlin/org/yapp/infra/userbook/repository/impl/UserBookRepositoryImpl.kt b/infra/src/main/kotlin/org/yapp/infra/userbook/repository/impl/UserBookRepositoryImpl.kt index 3cda1f8f..38320e2b 100644 --- a/infra/src/main/kotlin/org/yapp/infra/userbook/repository/impl/UserBookRepositoryImpl.kt +++ b/infra/src/main/kotlin/org/yapp/infra/userbook/repository/impl/UserBookRepositoryImpl.kt @@ -25,8 +25,8 @@ class UserBookRepositoryImpl( return jpaUserBookRepository.findByBookIdAndUserId(bookId, userId)?.toDomain() } - override fun findByIdAndUserId(id: UUID, userId: UUID): UserBook? { - return jpaUserBookRepository.findByIdAndUserId(id, userId)?.toDomain() + override fun existsByIdAndUserId(id: UUID, userId: UUID): Boolean { + return jpaUserBookRepository.existsByIdAndUserId(id, userId) } override fun findById(id: UUID): UserBook? {