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 fb3323d3..82e5a1d8 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 @@ -3,7 +3,6 @@ package org.yapp.apis.book.dto.request import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.Size import org.yapp.apis.book.dto.response.BookDetailResponse -import org.yapp.domain.book.Book data class BookCreateRequest private constructor( @field:NotBlank(message = "ISBN은 필수입니다.") @@ -32,7 +31,7 @@ data class BookCreateRequest private constructor( companion object { - fun create(bookDetail: BookDetailResponse): BookCreateRequest { + fun from(bookDetail: BookDetailResponse): BookCreateRequest { val finalIsbn = bookDetail.isbn ?: bookDetail.isbn13 ?: throw IllegalArgumentException("ISBN이 존재하지 않습니다.") 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 new file mode 100644 index 00000000..386290f0 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/book/dto/request/UpsertUserBookRequest.kt @@ -0,0 +1,35 @@ +package org.yapp.apis.book.dto.request + +import org.yapp.apis.book.dto.response.BookCreateResponse +import org.yapp.apis.book.dto.response.UserBookResponse +import org.yapp.domain.userbook.BookStatus +import java.util.UUID + + +data class UpsertUserBookRequest private constructor( + val userId: UUID, + val bookIsbn: String, + val bookTitle: String, + val bookAuthor: String, + val bookPublisher: String, + val bookCoverImageUrl: String, + val status: BookStatus +) { + companion object { + fun of( + userId: UUID, + bookCreateResponse: BookCreateResponse, + status: BookStatus + ): UpsertUserBookRequest { + return UpsertUserBookRequest( + userId = userId, + bookIsbn = bookCreateResponse.isbn, + bookTitle = bookCreateResponse.title, + bookAuthor = bookCreateResponse.author, + bookPublisher = bookCreateResponse.publisher, + bookCoverImageUrl = bookCreateResponse.coverImageUrl, + status = status + ) + } + } +} diff --git a/apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookCreateResponse.kt b/apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookCreateResponse.kt new file mode 100644 index 00000000..2da8a59e --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookCreateResponse.kt @@ -0,0 +1,23 @@ +package org.yapp.apis.book.dto.response + +import org.yapp.domain.book.vo.BookVO + +data class BookCreateResponse private constructor( + val isbn: String, + val title: String, + val author: String, + val publisher: String, + val coverImageUrl: String +) { + companion object { + fun from(bookVO: BookVO): BookCreateResponse { + return BookCreateResponse( + isbn = bookVO.isbn, + title = bookVO.title, + author = bookVO.author, + publisher = bookVO.publisher, + coverImageUrl = bookVO.coverImageUrl + ) + } + } +} 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 3ea5900b..baa96ea9 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 @@ -29,6 +29,7 @@ data class BookDetailResponse private constructor( ) { companion object { + fun from(response: AladinBookDetailResponse): BookDetailResponse { val bookItem = response.item?.firstOrNull() ?: throw IllegalArgumentException("No book item found in detail response.") diff --git a/apis/src/main/kotlin/org/yapp/apis/book/dto/response/UserBookResponse.kt b/apis/src/main/kotlin/org/yapp/apis/book/dto/response/UserBookResponse.kt index b325bdaa..c12d8e6c 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/dto/response/UserBookResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/dto/response/UserBookResponse.kt @@ -1,9 +1,9 @@ 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.* +import java.util.UUID data class UserBookResponse private constructor( val userBookId: UUID, @@ -20,7 +20,7 @@ data class UserBookResponse private constructor( companion object { fun from( - userBook: UserBook, + userBook: UserBookVO, ): UserBookResponse { return UserBookResponse( userBookId = userBook.id, 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 5e119b14..97ce18dc 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 @@ -2,17 +2,17 @@ package org.yapp.apis.book.service import org.springframework.stereotype.Service import org.yapp.apis.book.dto.request.BookCreateRequest -import org.yapp.domain.book.Book +import org.yapp.apis.book.dto.response.BookCreateResponse import org.yapp.domain.book.BookDomainService @Service class BookManagementService( private val bookDomainService: BookDomainService ) { - fun findOrCreateBook(request: BookCreateRequest): Book { + fun findOrCreateBook(request: BookCreateRequest): BookCreateResponse { val isbn = request.validIsbn() - return bookDomainService.findByIsbn(isbn) + val bookVO = bookDomainService.findByIsbn(isbn) ?: bookDomainService.save( isbn = isbn, title = request.validTitle(), @@ -22,6 +22,7 @@ class BookManagementService( publicationYear = request.publicationYear, description = request.description ) + return BookCreateResponse.from(bookVO) } } 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 23972439..df9690db 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,18 +1,35 @@ package org.yapp.apis.book.service import org.springframework.stereotype.Service +import org.yapp.apis.book.dto.request.UpsertUserBookRequest +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 org.yapp.domain.userbook.vo.UserBookVO import java.util.* @Service class UserBookService( private val userBookDomainService: UserBookDomainService ) { - fun upsertUserBook(userId: UUID, book: Book, status: BookStatus) = - userBookDomainService.upsertUserBook(userId, book, status) + fun upsertUserBook(upsertUserBookRequest: UpsertUserBookRequest): UserBookResponse = + UserBookResponse.from( + userBookDomainService.upsertUserBook( + upsertUserBookRequest.userId, + upsertUserBookRequest.bookIsbn, + upsertUserBookRequest.bookPublisher, + upsertUserBookRequest.bookAuthor, + upsertUserBookRequest.bookTitle, + upsertUserBookRequest.bookCoverImageUrl, + upsertUserBookRequest.status + ) + ) - fun findAllUserBooks(userId: UUID) = - userBookDomainService.findAllUserBooks(userId) + fun findAllUserBooks(userId: UUID): List { + val userBooks: List = userBookDomainService.findAllUserBooks(userId) + return userBooks.map { userBook: UserBookVO -> + UserBookResponse.from(userBook) + } + } } 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 e0e2d713..c5977d63 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 @@ -11,6 +11,7 @@ import org.yapp.apis.book.dto.response.BookDetailResponse import org.yapp.apis.book.dto.response.BookSearchResponse 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 @@ -20,10 +21,12 @@ import java.util.UUID @UseCase @Transactional(readOnly = true) class BookUseCase( - private val userAuthService: UserAuthService, - private val userBookService: UserBookService, + @Qualifier(BookQueryServiceQualifier.ALADIN) private val bookQueryService: BookQueryService, + + private val userAuthService: UserAuthService, + private val userBookService: UserBookService, private val bookManagementService: BookManagementService ) { fun searchBooks(request: BookSearchRequest): BookSearchResponse { @@ -38,13 +41,17 @@ class BookUseCase( fun upsertBookToMyLibrary(userId: UUID, request: UserBookRegisterRequest): UserBookResponse { userAuthService.validateUserExists(userId) - val detail = bookQueryService.getBookDetail(BookDetailRequest.of(request.validBookIsbn())) - - val book = bookManagementService.findOrCreateBook(BookCreateRequest.create(detail)) + val bookDetailResponse = bookQueryService.getBookDetail(BookDetailRequest.of(request.validBookIsbn())) - val userBook = userBookService.upsertUserBook(userId, book, request.bookStatus) + val bookCreateResponse = bookManagementService.findOrCreateBook(BookCreateRequest.from(bookDetailResponse)) + val upsertUserBookRequest = UpsertUserBookRequest.of( + userId = userId, + bookCreateResponse, + status = request.bookStatus + ) + val userBookResponse = userBookService.upsertUserBook(upsertUserBookRequest) - return UserBookResponse.from(userBook) + return userBookResponse } @@ -52,6 +59,5 @@ class BookUseCase( userAuthService.validateUserExists(userId) return userBookService.findAllUserBooks(userId) - .map(UserBookResponse::from) } } diff --git a/domain/src/main/kotlin/org/yapp/domain/book/BookDomainService.kt b/domain/src/main/kotlin/org/yapp/domain/book/BookDomainService.kt index f8176b9c..b8e22cde 100644 --- a/domain/src/main/kotlin/org/yapp/domain/book/BookDomainService.kt +++ b/domain/src/main/kotlin/org/yapp/domain/book/BookDomainService.kt @@ -1,13 +1,14 @@ package org.yapp.domain.book +import org.yapp.domain.book.vo.BookVO import org.yapp.globalutils.annotation.DomainService @DomainService class BookDomainService( private val bookRepository: BookRepository ) { - fun findByIsbn(isbn: String): Book? { - return bookRepository.findByIsbn(isbn) + fun findByIsbn(isbn: String): BookVO? { + return bookRepository.findByIsbn(isbn)?.let { BookVO.newInstance(it) } } fun save( @@ -18,12 +19,8 @@ class BookDomainService( coverImageUrl: String, publicationYear: Int? = null, description: String? = null - ): Book { - findByIsbn(isbn)?.let { - return it - } - - val book = Book.create( + ): BookVO { + val book = bookRepository.findByIsbn(isbn) ?: Book.create( isbn = isbn, title = title, author = author, @@ -33,6 +30,7 @@ class BookDomainService( description = description ) - return bookRepository.save(book) + val savedBook = bookRepository.save(book) + return BookVO.newInstance(savedBook) } } diff --git a/domain/src/main/kotlin/org/yapp/domain/book/vo/BookVO.kt b/domain/src/main/kotlin/org/yapp/domain/book/vo/BookVO.kt new file mode 100644 index 00000000..2262ecd5 --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/book/vo/BookVO.kt @@ -0,0 +1,36 @@ +package org.yapp.domain.book.vo + +import org.yapp.domain.book.Book + +data class BookVO private constructor( + val isbn: String, + val title: String, + val author: String, + val publisher: String, + val coverImageUrl: String, + val publicationYear: Int?, + val description: String? +) { + init { + require(isbn.isNotBlank()) { "ISBN은 비어 있을 수 없습니다." } + require(title.isNotBlank()) { "제목은 비어 있을 수 없습니다." } + require(author.isNotBlank()) { "저자는 비어 있을 수 없습니다." } + publicationYear?.let { require(it > 0) { "출판 연도는 0보다 커야 합니다." } } + } + + companion object { + fun newInstance( + book: Book + ): BookVO { + return BookVO( + book.isbn, + book.title, + book.author, + book.publisher, + book.coverImageUrl, + book.publicationYear, + book.description + ) + } + } +} diff --git a/domain/src/main/kotlin/org/yapp/domain/userbook/UserBook.kt b/domain/src/main/kotlin/org/yapp/domain/userbook/UserBook.kt index 5d3ac971..5c8d00e7 100644 --- a/domain/src/main/kotlin/org/yapp/domain/userbook/UserBook.kt +++ b/domain/src/main/kotlin/org/yapp/domain/userbook/UserBook.kt @@ -27,18 +27,22 @@ data class UserBook private constructor( companion object { fun create( userId: UUID, - book: Book, + coverImageUrl: String, + bookIsbn: String, + publisher: String, + title: String, + author: String, initialStatus: BookStatus = BookStatus.BEFORE_READING ): UserBook { val now = LocalDateTime.now() return UserBook( id = UuidGenerator.create(), - coverImageUrl = book.coverImageUrl, - publisher = book.publisher, - title = book.title, - author = book.author, + coverImageUrl = coverImageUrl, + publisher = publisher, + title = title, + author = author, userId = userId, - bookIsbn = book.isbn, + bookIsbn = bookIsbn, status = initialStatus, createdAt = now, updatedAt = now, 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 e8acaa4f..78db78c0 100644 --- a/domain/src/main/kotlin/org/yapp/domain/userbook/UserBookDomainService.kt +++ b/domain/src/main/kotlin/org/yapp/domain/userbook/UserBookDomainService.kt @@ -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 @@ -8,19 +9,32 @@ import java.util.UUID class UserBookDomainService( private val userBookRepository: UserBookRepository ) { + fun upsertUserBook( + userId: UUID, + bookIsbn: String, + bookTitle: String, + bookAuthor: String, + bookPublisher: String, + bookCoverImageUrl: String, + status: BookStatus + ): UserBookVO { + val userBook = userBookRepository.findByUserIdAndBookIsbn(userId, bookIsbn) + ?.apply { updateStatus(status) } + ?: UserBook.create( + userId = userId, + bookIsbn = bookIsbn, + title = bookTitle, + author = bookAuthor, + publisher = bookPublisher, + coverImageUrl = bookCoverImageUrl, + ) - fun upsertUserBook(userId: UUID, book: Book, status: BookStatus): UserBook { - val existing = userBookRepository.findByUserIdAndBookIsbn(userId, book.isbn) - return if (existing != null) { - val updated = existing.updateStatus(status) - userBookRepository.save(updated) - } else { - val created = UserBook.create(userId, book, status) - userBookRepository.save(created) - } + val savedUserBook = userBookRepository.save(userBook) + return UserBookVO.newInstance(savedUserBook) } - fun findAllUserBooks(userId: UUID): List { + fun findAllUserBooks(userId: UUID): List { return userBookRepository.findAllByUserId(userId) + .map(UserBookVO::newInstance) } } diff --git a/domain/src/main/kotlin/org/yapp/domain/userbook/vo/UserBookVO.kt b/domain/src/main/kotlin/org/yapp/domain/userbook/vo/UserBookVO.kt new file mode 100644 index 00000000..99d6fa0a --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/userbook/vo/UserBookVO.kt @@ -0,0 +1,49 @@ +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 +) { + init { + require(bookIsbn.isNotBlank()) { "도서 ISBN은 비어 있을 수 없습니다." } + require(coverImageUrl.isNotBlank()) { "표지 이미지 URL은 비어 있을 수 없습니다." } + require(publisher.isNotBlank()) { "출판사는 비어 있을 수 없습니다." } + require(title.isNotBlank()) { "도서 제목은 비어 있을 수 없습니다." } + require(author.isNotBlank()) { "저자는 비어 있을 수 없습니다." } + require(createdAt.isBefore(updatedAt) || createdAt == updatedAt) { + "생성일(createdAt)은 수정일(updatedAt)보다 이후일 수 없습니다." + } + } + + 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 + ) + } + } +}