diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/DeleteTokenRequest.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/DeleteTokenRequest.kt index 05d3e75a..4ea5c339 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/DeleteTokenRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/DeleteTokenRequest.kt @@ -8,7 +8,7 @@ import org.yapp.apis.auth.dto.response.RefreshTokenResponse name = "DeleteTokenRequest", description = "Request DTO for deleting a refresh token" ) -data class DeleteTokenRequest( +data class DeleteTokenRequest private constructor( @field:NotBlank(message = "Refresh token must not be blank.") @Schema(description = "Refresh token to be deleted", example = "eyJhbGciOiJIUz...") val refreshToken: String? = null diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/FindUserIdentityRequest.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/FindUserIdentityRequest.kt index e52041c3..ffff9255 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/FindUserIdentityRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/FindUserIdentityRequest.kt @@ -9,7 +9,7 @@ import java.util.* name = "FindUserIdentityRequest", description = "Request DTO to retrieve user identity information using userId" ) -data class FindUserIdentityRequest( +data class FindUserIdentityRequest private constructor( @Schema( description = "User ID (UUID format)", example = "a1b2c3d4-e5f6-7890-1234-56789abcdef0" diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/TokenGenerateRequest.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/TokenGenerateRequest.kt index 69a59bc9..37b71ede 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/TokenGenerateRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/TokenGenerateRequest.kt @@ -9,7 +9,7 @@ import java.util.* name = "TokenGenerateRequest", description = "DTO containing information required to save the generated refresh token" ) -data class TokenGenerateRequest( +data class TokenGenerateRequest private constructor( @field:NotNull(message = "userId must not be null") @Schema(description = "User ID", example = "f6b7d490-1b1a-4b9f-8e8e-27f8e3a5dafa") val userId: UUID? = null, diff --git a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/UserBooksByIsbnsRequest.kt b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/UserBooksByIsbnsRequest.kt index 97a3b739..c2586232 100644 --- a/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/UserBooksByIsbnsRequest.kt +++ b/apis/src/main/kotlin/org/yapp/apis/auth/dto/request/UserBooksByIsbnsRequest.kt @@ -9,7 +9,7 @@ import java.util.UUID name = "UserBooksByIsbnsRequest", description = "Request DTO for finding user books by user ID and a list of ISBNs" ) -data class UserBooksByIsbnsRequest( +data class UserBooksByIsbnsRequest private constructor( @Schema( description = "사용자 ID", example = "1" 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 f678e9c5..220d950e 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 @@ -1,7 +1,6 @@ package org.yapp.apis.book.controller import jakarta.validation.Valid -import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.data.domain.Sort import org.springframework.data.web.PageableDefault @@ -23,6 +22,7 @@ import org.yapp.apis.book.dto.response.UserBookPageResponse import org.yapp.apis.book.dto.response.UserBookResponse import org.yapp.apis.book.usecase.BookUseCase import org.yapp.domain.userbook.BookStatus +import org.yapp.domain.userbook.UserBookSortType import java.util.UUID @RestController @@ -62,7 +62,7 @@ class BookController( override fun getUserLibraryBooks( @AuthenticationPrincipal userId: UUID, @RequestParam(required = false) status: BookStatus?, - @RequestParam(required = false) sort: String?, + @RequestParam(required = false) sort: UserBookSortType?, @PageableDefault(size = 10, sort = ["createdAt"], direction = Sort.Direction.DESC) pageable: Pageable ): ResponseEntity { diff --git a/apis/src/main/kotlin/org/yapp/apis/book/controller/BookControllerApi.kt b/apis/src/main/kotlin/org/yapp/apis/book/controller/BookControllerApi.kt index d7ed95e3..348bac2d 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/controller/BookControllerApi.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/controller/BookControllerApi.kt @@ -27,6 +27,7 @@ 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.domain.userbook.BookStatus +import org.yapp.domain.userbook.UserBookSortType import org.yapp.globalutils.exception.ErrorResponse import java.util.UUID @@ -140,7 +141,7 @@ interface BookControllerApi { fun getUserLibraryBooks( @AuthenticationPrincipal userId: UUID, @RequestParam(required = false) status: BookStatus?, - @RequestParam(required = false) sort: String?, + @RequestParam(required = false) sort: UserBookSortType?, @PageableDefault(size = 10, sort = ["createdAt"], direction = Sort.Direction.DESC) pageable: Pageable ): ResponseEntity 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 82e5a1d8..62afb3bb 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 @@ -22,7 +22,7 @@ data class BookCreateRequest private constructor( @field:Size(max = 2048, message = "표지 URL은 2048자 이내여야 합니다.") val coverImageUrl: String, - val description: String? = null, + val description: String? = null ) { fun validIsbn(): String = isbn!! fun validTitle(): String = title!! @@ -42,7 +42,7 @@ data class BookCreateRequest private constructor( publisher = bookDetail.publisher, publicationYear = parsePublicationYear(bookDetail.pubDate), coverImageUrl = bookDetail.cover, - description = bookDetail.description + description = bookDetail.description, ) } 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 e1cb0a7e..cd987615 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,13 +1,13 @@ 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? = null, + val bookId: UUID? = null, val bookIsbn: String? = null, val bookTitle: String? = null, val bookAuthor: String? = null, @@ -16,6 +16,7 @@ data class UpsertUserBookRequest private constructor( val status: BookStatus? = null ) { fun validUserId(): UUID = userId!! + fun validBookId(): UUID = bookId!! fun validBookIsbn(): String = bookIsbn!! fun validBookTitle(): String = bookTitle!! fun validBookAuthor(): String = bookAuthor!! @@ -27,10 +28,11 @@ data class UpsertUserBookRequest private constructor( fun of( userId: UUID, bookCreateResponse: BookCreateResponse, - status: BookStatus + status: BookStatus, ): UpsertUserBookRequest { return UpsertUserBookRequest( userId = userId, + bookId = bookCreateResponse.bookId, bookIsbn = bookCreateResponse.isbn, bookTitle = bookCreateResponse.title, bookAuthor = bookCreateResponse.author, 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 index 2cb3a052..449c0bf5 100644 --- 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 @@ -2,21 +2,25 @@ package org.yapp.apis.book.dto.response import org.yapp.domain.book.vo.BookInfoVO +import java.util.UUID + data class BookCreateResponse private constructor( + val bookId: UUID, val isbn: String, val title: String, val author: String, val publisher: String, - val coverImageUrl: String + val coverImageUrl: String, ) { companion object { fun from(bookVO: BookInfoVO): BookCreateResponse { return BookCreateResponse( + bookId = bookVO.id.value, isbn = bookVO.isbn.value, title = bookVO.title, author = bookVO.author, publisher = bookVO.publisher, - coverImageUrl = bookVO.coverImageUrl + 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 baa96ea9..63bcf923 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 @@ -25,7 +25,7 @@ data class BookDetailResponse private constructor( val cover: String, val categoryId: Int?, val categoryName: String?, - val publisher: String + val publisher: String, ) { companion object { diff --git a/apis/src/main/kotlin/org/yapp/apis/book/dto/response/UserBookPageResponse.kt b/apis/src/main/kotlin/org/yapp/apis/book/dto/response/UserBookPageResponse.kt index ed964dc2..0f30d8ce 100644 --- a/apis/src/main/kotlin/org/yapp/apis/book/dto/response/UserBookPageResponse.kt +++ b/apis/src/main/kotlin/org/yapp/apis/book/dto/response/UserBookPageResponse.kt @@ -16,7 +16,10 @@ data class UserBookPageResponse private constructor( val readingCount: Long, @Schema(description = "완독한 책 개수") - val completedCount: Long + val completedCount: Long, + + @Schema(description = "총 책 개수") + val totalCount: Long ) { companion object { fun of( @@ -25,11 +28,13 @@ data class UserBookPageResponse private constructor( readingCount: Long, completedCount: Long ): UserBookPageResponse { + val totalCount = beforeReadingCount + readingCount + completedCount return UserBookPageResponse( books = books, beforeReadingCount = beforeReadingCount, readingCount = readingCount, - completedCount = completedCount + completedCount = completedCount, + totalCount = totalCount ) } } diff --git a/apis/src/main/kotlin/org/yapp/apis/book/exception/UserBookErrorCode.kt b/apis/src/main/kotlin/org/yapp/apis/book/exception/UserBookErrorCode.kt new file mode 100644 index 00000000..46e23880 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/book/exception/UserBookErrorCode.kt @@ -0,0 +1,16 @@ +package org.yapp.apis.book.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/apis/src/main/kotlin/org/yapp/apis/book/exception/UserBookNotFoundException.kt b/apis/src/main/kotlin/org/yapp/apis/book/exception/UserBookNotFoundException.kt new file mode 100644 index 00000000..7598e468 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/book/exception/UserBookNotFoundException.kt @@ -0,0 +1,8 @@ +package org.yapp.apis.book.exception + +import org.yapp.globalutils.exception.CommonException + +class UserBookNotFoundException( + errorCode: UserBookErrorCode, + message: String? = null +) : CommonException(errorCode, message) 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 c0e5f8f5..290efda6 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 @@ -7,10 +7,12 @@ import org.yapp.apis.auth.dto.request.UserBooksByIsbnsRequest import org.yapp.apis.book.dto.response.UserBookPageResponse import org.yapp.apis.book.dto.response.UserBookResponse import org.yapp.apis.book.dto.request.UpsertUserBookRequest +import org.yapp.apis.book.exception.UserBookErrorCode +import org.yapp.apis.book.exception.UserBookNotFoundException import org.yapp.domain.userbook.BookStatus +import org.yapp.domain.userbook.UserBook import org.yapp.domain.userbook.UserBookDomainService -import org.yapp.domain.userbook.vo.UserBookInfoVO -import org.yapp.domain.userbook.vo.UserBookStatusCountsVO +import org.yapp.domain.userbook.UserBookSortType import java.util.UUID @@ -21,6 +23,7 @@ class UserBookService( fun upsertUserBook(upsertUserBookRequest: UpsertUserBookRequest): UserBookResponse { val userBookInfoVO = userBookDomainService.upsertUserBook( upsertUserBookRequest.validUserId(), + upsertUserBookRequest.validBookId(), upsertUserBookRequest.validBookIsbn(), upsertUserBookRequest.validBookTitle(), upsertUserBookRequest.validBookAuthor(), @@ -31,13 +34,15 @@ class UserBookService( return UserBookResponse.from(userBookInfoVO) } - fun findAllUserBooks(userId: UUID): List { - val userBooks: List = userBookDomainService.findAllUserBooks(userId) - return userBooks.map { userBook: UserBookInfoVO -> - UserBookResponse.from(userBook) - } + fun validateUserBookExists(userId: UUID, userBookId: UUID): UserBook { + return userBookDomainService.findByIdAndUserId(userBookId, userId) + ?: throw UserBookNotFoundException( + UserBookErrorCode.USER_BOOK_NOT_FOUND, + "User book not found with id: $userBookId and userId: $userId" + ) } + fun findAllByUserIdAndBookIsbnIn(userBooksByIsbnsRequest: UserBooksByIsbnsRequest): List { val userBooks = userBookDomainService.findAllByUserIdAndBookIsbnIn( userBooksByIsbnsRequest.validUserId(), @@ -49,7 +54,7 @@ class UserBookService( private fun findUserBooksByDynamicCondition( userId: UUID, status: BookStatus?, - sort: String?, + sort: UserBookSortType?, pageable: Pageable ): Page { val page = userBookDomainService.findUserBooksByDynamicCondition(userId, status, sort, pageable) @@ -59,7 +64,7 @@ class UserBookService( fun findUserBooksByDynamicConditionWithStatusCounts( userId: UUID, status: BookStatus?, - sort: String?, + sort: UserBookSortType?, pageable: Pageable ): UserBookPageResponse { val userBookResponsePage = findUserBooksByDynamicCondition(userId, status, sort, pageable) 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 4cd095fd..6be48d7f 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 @@ -20,6 +20,7 @@ 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 @@ -63,7 +64,7 @@ class BookUseCase( val upsertUserBookRequest = UpsertUserBookRequest.of( userId = userId, bookCreateResponse, - status = request.bookStatus + status = request.bookStatus, ) val userBookResponse = userBookService.upsertUserBook(upsertUserBookRequest) @@ -73,7 +74,7 @@ class BookUseCase( fun getUserLibraryBooks( userId: UUID, status: BookStatus?, - sort: String?, + sort: UserBookSortType?, pageable: Pageable ): UserBookPageResponse { userAuthService.validateUserExists(userId) diff --git a/apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordController.kt b/apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordController.kt new file mode 100644 index 00000000..9d04735d --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordController.kt @@ -0,0 +1,60 @@ +package org.yapp.apis.readingrecord.controller + +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Sort +import org.springframework.data.web.PageableDefault +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController +import org.yapp.apis.readingrecord.dto.request.CreateReadingRecordRequest +import org.yapp.apis.readingrecord.dto.response.ReadingRecordResponse +import org.yapp.apis.readingrecord.usecase.ReadingRecordUseCase +import org.yapp.domain.readingrecord.ReadingRecordSortType +import java.util.UUID +import jakarta.validation.Valid + +@RestController +@RequestMapping("/api/v1/reading-records") +class ReadingRecordController( + private val readingRecordUseCase: ReadingRecordUseCase +) : ReadingRecordControllerApi { + + @PostMapping("/{userBookId}") + override fun createReadingRecord( + @AuthenticationPrincipal userId: UUID, + @PathVariable userBookId: UUID, + @Valid @RequestBody request: CreateReadingRecordRequest + ): ResponseEntity { + val response = readingRecordUseCase.createReadingRecord( + userId = userId, + userBookId = userBookId, + request = request + ) + return ResponseEntity.status(HttpStatus.CREATED).body(response) + } + + @GetMapping("/{userBookId}") + override fun getReadingRecords( + @AuthenticationPrincipal userId: UUID, + @PathVariable userBookId: UUID, + @RequestParam(required = false) sort: ReadingRecordSortType?, + @PageableDefault(size = 10, sort = ["createdAt"], direction = Sort.Direction.DESC) + pageable: Pageable + ): ResponseEntity> { + val response = readingRecordUseCase.getReadingRecordsByUserBookId( + userId = userId, + userBookId = userBookId, + sort = sort, + pageable = pageable + ) + return ResponseEntity.ok(response) + } +} diff --git a/apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerApi.kt b/apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerApi.kt new file mode 100644 index 00000000..54973e63 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerApi.kt @@ -0,0 +1,89 @@ +package org.yapp.apis.readingrecord.controller + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Sort +import org.springframework.data.web.PageableDefault +import org.springframework.http.ResponseEntity +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.yapp.apis.readingrecord.dto.request.CreateReadingRecordRequest +import org.yapp.apis.readingrecord.dto.response.ReadingRecordResponse +import org.yapp.domain.readingrecord.ReadingRecordSortType +import org.yapp.globalutils.exception.ErrorResponse +import java.util.UUID + +@Tag(name = "Reading Records", description = "독서 기록 관련 API") +@RequestMapping("/api/v1/reading-records") +interface ReadingRecordControllerApi { + + @Operation( + summary = "독서 기록 생성", + description = "사용자의 책에 대한 독서 기록을 생성합니다." + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "201", + description = "독서 기록 생성 성공", + content = [Content(schema = Schema(implementation = ReadingRecordResponse::class))] + ), + ApiResponse( + responseCode = "400", + description = "잘못된 요청", + content = [Content(schema = Schema(implementation = ErrorResponse::class))] + ), + ApiResponse( + responseCode = "404", + description = "사용자 또는 책을 찾을 수 없음", + content = [Content(schema = Schema(implementation = ErrorResponse::class))] + ) + ] + ) + @PostMapping("/{userBookId}") + fun createReadingRecord( + @AuthenticationPrincipal @Parameter(description = "인증된 사용자 ID") userId: UUID, + @PathVariable @Parameter(description = "독서 기록을 생성할 사용자 책 ID") userBookId: UUID, + @Valid @RequestBody @Parameter(description = "독서 기록 생성 요청 객체") request: CreateReadingRecordRequest + ): ResponseEntity + + @Operation( + summary = "독서 기록 목록 조회", + description = "사용자의 책에 대한 독서 기록을 페이징하여 조회합니다. 정렬은 페이지 번호 또는 최신 등록순으로 가능합니다." + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "독서 기록 목록 조회 성공", + content = [Content(schema = Schema(implementation = ReadingRecordResponse::class))] + ), + ApiResponse( + responseCode = "404", + description = "사용자 또는 책을 찾을 수 없음", + content = [Content(schema = Schema(implementation = ErrorResponse::class))] + ) + ] + ) + @GetMapping("/{userBookId}") + fun getReadingRecords( + @AuthenticationPrincipal @Parameter(description = "인증된 사용자 ID") userId: UUID, + @PathVariable @Parameter(description = "독서 기록을 조회할 사용자 책 ID") userBookId: UUID, + @RequestParam(required = false) @Parameter(description = "정렬 방식 (PAGE_NUMBER_ASC, PAGE_NUMBER_DESC, CREATED_DATE_ASC, CREATED_DATE_DESC)") sort: ReadingRecordSortType?, + @PageableDefault(size = 10, sort = ["createdAt"], direction = Sort.Direction.DESC) + @Parameter(description = "페이지네이션 정보 (페이지 번호, 페이지 크기, 정렬 방식)") pageable: Pageable + ): ResponseEntity> +} 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 new file mode 100644 index 00000000..3d441264 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequest.kt @@ -0,0 +1,35 @@ +package org.yapp.apis.readingrecord.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 + + +@Schema(description = "독서 기록 생성 요청") +data class CreateReadingRecordRequest private constructor( + @field:Min(1, message = "페이지 번호는 1 이상이어야 합니다.") + @field:Max(9999, message = "페이지 번호는 9999 이하여야 합니다.") + @Schema(description = "현재 읽은 페이지 번호", example = "42", required = true) + val pageNumber: Int? = null, + + @field:NotBlank(message = "기억에 남는 문장은 필수입니다.") + @field:Size(max = 1000, message = "기억에 남는 문장은 1000자를 초과할 수 없습니다.") + @Schema(description = "기억에 남는 문장", example = "이것은 기억에 남는 문장입니다.", required = true) + val quote: String? = null, + + @field:NotBlank(message = "감상평은 필수입니다.") + @field:Size(max = 1000, message = "감상평은 1000자를 초과할 수 없습니다.") + @Schema(description = "감상평", example = "이 책은 매우 인상적이었습니다.", required = true) + val review: String? = null, + + @field:Size(max = 3, message = "감정 태그는 최대 3개까지 가능합니다.") + @Schema(description = "감정 태그 목록 (최대 3개)", example = "[\"감동적\", \"슬픔\", \"희망\"]") + val emotionTags: List<@Size(max = 10, message = "감정 태그는 10자를 초과할 수 없습니다.") String> = emptyList() +) { + fun validPageNumber(): Int = pageNumber!! + fun validQuote(): String = quote!! + fun validReview(): String = review!! + fun validEmotionTags(): List = emotionTags +} diff --git a/apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/ReadingRecordResponse.kt b/apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/ReadingRecordResponse.kt new file mode 100644 index 00000000..e97d6ead --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/ReadingRecordResponse.kt @@ -0,0 +1,51 @@ +package org.yapp.apis.readingrecord.dto.response + +import io.swagger.v3.oas.annotations.media.Schema +import org.yapp.domain.readingrecord.vo.ReadingRecordInfoVO +import java.time.format.DateTimeFormatter +import java.util.UUID + + +@Schema(description = "독서 기록 응답") +data class ReadingRecordResponse private constructor( + @Schema(description = "독서 기록 ID", example = "123e4567-e89b-12d3-a456-426614174000") + val id: UUID, + + @Schema(description = "사용자 책 ID", example = "123e4567-e89b-12d3-a456-426614174000") + val userBookId: UUID, + + @Schema(description = "현재 읽은 페이지 번호", example = "42") + val pageNumber: Int, + + @Schema(description = "기억에 남는 문장", example = "이것은 기억에 남는 문장입니다.") + val quote: String, + + @Schema(description = "감상평", example = "이 책은 매우 인상적이었습니다.") + val review: String, + + @Schema(description = "감정 태그 목록", example = "[\"감동적\", \"슬픔\", \"희망\"]") + val emotionTags: List, + + @Schema(description = "생성 일시", example = "2023-01-01T12:00:00") + val createdAt: String, + + @Schema(description = "수정 일시", example = "2023-01-01T12:00:00") + val updatedAt: String +) { + companion object { + private val dateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME + + fun from(readingRecordInfoVO: ReadingRecordInfoVO): ReadingRecordResponse { + return ReadingRecordResponse( + id = readingRecordInfoVO.id.value, + userBookId = readingRecordInfoVO.userBookId.value, + pageNumber = readingRecordInfoVO.pageNumber.value, + quote = readingRecordInfoVO.quote.value, + review = readingRecordInfoVO.review.value, + emotionTags = readingRecordInfoVO.emotionTags, + createdAt = readingRecordInfoVO.createdAt.format(dateTimeFormatter), + updatedAt = readingRecordInfoVO.updatedAt.format(dateTimeFormatter) + ) + } + } +} 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 new file mode 100644 index 00000000..05c3af15 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordService.kt @@ -0,0 +1,50 @@ +package org.yapp.apis.readingrecord.service + +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.yapp.apis.readingrecord.dto.request.CreateReadingRecordRequest +import org.yapp.apis.readingrecord.dto.response.ReadingRecordResponse +import org.yapp.domain.book.BookDomainService +import org.yapp.domain.readingrecord.ReadingRecordDomainService +import org.yapp.domain.readingrecord.ReadingRecordSortType +import java.util.UUID + + +@Service +class ReadingRecordService( + private val readingRecordDomainService: ReadingRecordDomainService, + private val userBookService: UserBookService, + private val bookDomainService: BookDomainService +) { + + fun createReadingRecord( + userId: UUID, + userBookId: UUID, + request: CreateReadingRecordRequest + ): ReadingRecordResponse { + userBookService.validateUserBookExists(userId, userBookId) + + + val readingRecordInfoVO = readingRecordDomainService.createReadingRecord( + userBookId = userBookId, + pageNumber = request.validPageNumber(), + quote = request.validQuote(), + review = request.validReview(), + emotionTags = request.validEmotionTags() + ) + + return ReadingRecordResponse.from(readingRecordInfoVO) + } + + + fun getReadingRecordsByDynamicCondition( + userBookId: UUID, + sort: ReadingRecordSortType?, + pageable: Pageable + ): Page { + val page = readingRecordDomainService.findReadingRecordsByDynamicCondition(userBookId, sort, pageable) + return page.map { ReadingRecordResponse.from(it) } + } +} 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 new file mode 100644 index 00000000..0c4e4a9f --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCase.kt @@ -0,0 +1,61 @@ +package org.yapp.apis.readingrecord.usecase + +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.transaction.annotation.Transactional +import org.springframework.beans.factory.annotation.Qualifier +import org.yapp.apis.auth.service.UserAuthService +import org.yapp.apis.book.service.UserBookService +import org.yapp.apis.readingrecord.dto.request.CreateReadingRecordRequest +import org.yapp.apis.readingrecord.dto.response.ReadingRecordResponse +import org.yapp.apis.readingrecord.service.ReadingRecordService +import org.yapp.domain.readingrecord.ReadingRecordSortType +import org.yapp.globalutils.annotation.UseCase +import org.yapp.apis.book.constant.BookQueryServiceQualifier +import org.yapp.apis.book.service.BookQueryService +import org.yapp.domain.book.BookDomainService +import java.util.UUID + +@UseCase +@Transactional(readOnly = true) +class ReadingRecordUseCase( + private val readingRecordService: ReadingRecordService, + private val userAuthService: UserAuthService, + private val userBookService: UserBookService, + @Qualifier(BookQueryServiceQualifier.ALADIN) + private val bookQueryService: BookQueryService, + private val bookDomainService: BookDomainService +) { + @Transactional + fun createReadingRecord( + userId: UUID, + userBookId: UUID, + request: CreateReadingRecordRequest + ): ReadingRecordResponse { + userAuthService.validateUserExists(userId) + userBookService.validateUserBookExists(userId, userBookId) + + return readingRecordService.createReadingRecord( + userId = userId, + userBookId = userBookId, + request = request + ) + } + + fun getReadingRecordsByUserBookId( + userId: UUID, + userBookId: UUID, + sort: ReadingRecordSortType?, + pageable: Pageable + ): Page { + userAuthService.validateUserExists(userId) + + userBookService.validateUserBookExists(userId, userBookId) + + return readingRecordService.getReadingRecordsByDynamicCondition( + userBookId = userBookId, + sort = sort, + pageable = pageable + ) + } +} 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 35c90551..a86022c0 100644 --- a/domain/src/main/kotlin/org/yapp/domain/book/Book.kt +++ b/domain/src/main/kotlin/org/yapp/domain/book/Book.kt @@ -1,12 +1,15 @@ 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 /** * Represents a book in the domain model. */ data class Book private constructor( + val id: Id, val isbn: Isbn, val title: String, val author: String, @@ -26,9 +29,10 @@ data class Book private constructor( publisher: String, coverImageUrl: String, publicationYear: Int? = null, - description: String? = null + description: String? = null, ): Book { return Book( + id = Id.newInstance(UuidGenerator.create()), isbn = Isbn.newInstance(isbn), title = title, author = author, @@ -40,6 +44,7 @@ data class Book private constructor( } fun reconstruct( + id: Id, isbn: Isbn, title: String, author: String, @@ -52,6 +57,7 @@ data class Book private constructor( deletedAt: LocalDateTime? = null ): Book { return Book( + id = id, isbn = isbn, title = title, author = author, @@ -66,6 +72,13 @@ data class Book private constructor( } } + @JvmInline + value class Id(val value: UUID) { + companion object { + fun newInstance(value: UUID) = Id(value) + } + } + @JvmInline value class Isbn(val value: String) { companion object { 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 16c715e3..023145b7 100644 --- a/domain/src/main/kotlin/org/yapp/domain/book/BookDomainService.kt +++ b/domain/src/main/kotlin/org/yapp/domain/book/BookDomainService.kt @@ -5,13 +5,14 @@ import org.yapp.domain.book.exception.BookErrorCode import org.yapp.domain.book.exception.BookNotFoundException import org.yapp.domain.book.vo.BookInfoVO import org.yapp.globalutils.annotation.DomainService +import java.util.UUID @DomainService class BookDomainService( private val bookRepository: BookRepository ) { - fun findByIsbn(isbn: String): BookInfoVO { - val book = bookRepository.findById(isbn) ?: throw BookNotFoundException(BookErrorCode.BOOK_NOT_FOUND) + fun findById(id: UUID): BookInfoVO { + val book = bookRepository.findById(id) ?: throw BookNotFoundException(BookErrorCode.BOOK_NOT_FOUND) return BookInfoVO.newInstance(book) } @@ -40,7 +41,7 @@ class BookDomainService( } private fun findByIsbnOrNull(isbn: String): BookInfoVO? { - val book = bookRepository.findById(isbn) + val book = bookRepository.findByIsbn(isbn) return book?.let { BookInfoVO.newInstance(it) } } @@ -52,9 +53,9 @@ class BookDomainService( publisher: String, coverImageUrl: String, publicationYear: Int? = null, - description: String? = null + description: String? = null, ): BookInfoVO { - if (bookRepository.existsById(isbn)) { + if (bookRepository.existsByIsbn(isbn)) { throw BookAlreadyExistsException(BookErrorCode.BOOK_ALREADY_EXISTS) } diff --git a/domain/src/main/kotlin/org/yapp/domain/book/BookRepository.kt b/domain/src/main/kotlin/org/yapp/domain/book/BookRepository.kt index b89d132d..818a866d 100644 --- a/domain/src/main/kotlin/org/yapp/domain/book/BookRepository.kt +++ b/domain/src/main/kotlin/org/yapp/domain/book/BookRepository.kt @@ -1,7 +1,11 @@ package org.yapp.domain.book +import java.util.UUID + interface BookRepository { - fun findById(id: String): Book? - fun existsById(id: String): Boolean + fun findById(id: UUID): Book? + fun existsById(id: UUID): Boolean + fun findByIsbn(isbn: String): Book? + fun existsByIsbn(isbn: String): Boolean fun save(book: Book): Book } diff --git a/domain/src/main/kotlin/org/yapp/domain/book/vo/BookInfoVO.kt b/domain/src/main/kotlin/org/yapp/domain/book/vo/BookInfoVO.kt index 34de4174..be497d2c 100644 --- a/domain/src/main/kotlin/org/yapp/domain/book/vo/BookInfoVO.kt +++ b/domain/src/main/kotlin/org/yapp/domain/book/vo/BookInfoVO.kt @@ -3,13 +3,14 @@ package org.yapp.domain.book.vo import org.yapp.domain.book.Book data class BookInfoVO private constructor( + val id: Book.Id, val isbn: Book.Isbn, val title: String, val author: String, val publisher: String, val coverImageUrl: String, val publicationYear: Int?, - val description: String? + val description: String?, ) { init { require(title.isNotBlank()) { "제목은 비어 있을 수 없습니다." } @@ -22,13 +23,14 @@ data class BookInfoVO private constructor( book: Book ): BookInfoVO { return BookInfoVO( + book.id, book.isbn, book.title, book.author, book.publisher, book.coverImageUrl, book.publicationYear, - book.description + book.description, ) } } diff --git a/domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecord.kt b/domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecord.kt new file mode 100644 index 00000000..5d85d999 --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecord.kt @@ -0,0 +1,119 @@ +package org.yapp.domain.readingrecord + +import org.yapp.globalutils.util.UuidGenerator +import java.time.LocalDateTime +import java.util.* + +data class ReadingRecord private constructor( + val id: Id, + val userBookId: UserBookId, + val pageNumber: PageNumber, + val quote: Quote, + val review: Review, + val emotionTags: List = emptyList(), + val createdAt: LocalDateTime? = null, + val updatedAt: LocalDateTime? = null, + val deletedAt: LocalDateTime? = null, +) { + + + companion object { + fun create( + userBookId: UUID, + pageNumber: Int, + quote: String, + review: String, + emotionTags: List = emptyList() + ): ReadingRecord { + return ReadingRecord( + id = Id.newInstance(UuidGenerator.create()), + userBookId = UserBookId.newInstance(userBookId), + pageNumber = PageNumber.newInstance(pageNumber), + quote = Quote.newInstance(quote), + review = Review.newInstance(review), + emotionTags = emotionTags.map { EmotionTag.newInstance(it) } + ) + } + + fun reconstruct( + id: Id, + userBookId: UserBookId, + pageNumber: PageNumber, + quote: Quote, + review: Review, + emotionTags: List = emptyList(), + createdAt: LocalDateTime? = null, + updatedAt: LocalDateTime? = null, + deletedAt: LocalDateTime? = null + ): ReadingRecord { + return ReadingRecord( + id = id, + userBookId = userBookId, + pageNumber = pageNumber, + quote = quote, + review = review, + emotionTags = emotionTags, + createdAt = createdAt, + updatedAt = updatedAt, + deletedAt = deletedAt + ) + } + } + + @JvmInline + value class Id(val value: UUID) { + companion object { + fun newInstance(value: UUID) = Id(value) + } + } + + @JvmInline + value class UserBookId(val value: UUID) { + companion object { + fun newInstance(value: UUID) = UserBookId(value) + } + } + + @JvmInline + value class PageNumber(val value: Int) { + companion object { + fun newInstance(value: Int): PageNumber { + require(value in 1..9999) { "Page number must be between 1 and 9999" } + return PageNumber(value) + } + } + } + + @JvmInline + value class Quote(val value: String) { + companion object { + fun newInstance(value: String): Quote { + require(value.isNotBlank()) { "Quote cannot be blank" } + require(value.length <= 1000) { "Quote cannot exceed 1000 characters" } + return Quote(value) + } + } + } + + @JvmInline + value class Review(val value: String) { + companion object { + fun newInstance(value: String): Review { + require(value.isNotBlank()) { "Review cannot be blank" } + require(value.length <= 1000) { "Review cannot exceed 1000 characters" } + return Review(value) + } + } + } + + @JvmInline + value class EmotionTag(val value: String) { + companion object { + fun newInstance(value: String): EmotionTag { + require(value.isNotBlank()) { "Emotion tag cannot be blank" } + require(value.length <= 10) { "Emotion tag cannot exceed 10 characters" } + return EmotionTag(value) + } + } + } +} diff --git a/domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt b/domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt new file mode 100644 index 00000000..7326949e --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt @@ -0,0 +1,66 @@ +package org.yapp.domain.readingrecord + +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.yapp.domain.readingrecord.vo.ReadingRecordInfoVO +import org.yapp.domain.readingrecordtag.ReadingRecordTag +import org.yapp.domain.readingrecordtag.ReadingRecordTagRepository +import org.yapp.domain.tag.Tag +import org.yapp.domain.tag.TagRepository +import org.yapp.globalutils.annotation.DomainService +import java.util.UUID + +@DomainService +class ReadingRecordDomainService( + private val readingRecordRepository: ReadingRecordRepository, + private val tagRepository: TagRepository, + private val readingRecordTagRepository: ReadingRecordTagRepository +) { + + fun createReadingRecord( + userBookId: UUID, + pageNumber: Int, + quote: String, + review: String, + emotionTags: List + ): ReadingRecordInfoVO { + + val readingRecord = ReadingRecord.create( + userBookId = userBookId, + pageNumber = pageNumber, + quote = quote, + review = review + ) + + val savedReadingRecord = readingRecordRepository.save(readingRecord) + + val tags = emotionTags.map { tagName -> + tagRepository.findByName(tagName) ?: tagRepository.save(Tag.create(tagName)) + } + + val readingRecordTags = tags.map { + ReadingRecordTag.create( + readingRecordId = savedReadingRecord.id.value, + tagId = it.id.value + ) + } + readingRecordTagRepository.saveAll(readingRecordTags) + + return ReadingRecordInfoVO.newInstance(savedReadingRecord, tags.map { it.name }) + } + + fun findReadingRecordsByDynamicCondition( + userBookId: UUID, + sort: ReadingRecordSortType?, + pageable: Pageable + ): Page { + val page = readingRecordRepository.findReadingRecordsByDynamicCondition(userBookId, sort, pageable) + return page.map { readingRecord -> + val readingRecordTags = readingRecordTagRepository.findByReadingRecordId(readingRecord.id.value) + val tagIds = readingRecordTags.map { it.tagId.value } + val tags = tagRepository.findByIds(tagIds) + ReadingRecordInfoVO.newInstance(readingRecord, tags.map { it.name }) + } + } + +} diff --git a/domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordRepository.kt b/domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordRepository.kt new file mode 100644 index 00000000..daf2575e --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordRepository.kt @@ -0,0 +1,33 @@ +package org.yapp.domain.readingrecord + +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import java.util.UUID + + +interface ReadingRecordRepository { + + fun save(readingRecord: ReadingRecord): ReadingRecord + + + fun findById(id: UUID): ReadingRecord? + + + fun findAllByUserBookId(userBookId: UUID): List + + + fun findAllByUserBookId(userBookId: UUID, pageable: Pageable): Page + + + fun findAllByUserBookIdIn(userBookIds: List): List + + + fun countByUserBookId(userBookId: UUID): Long + + + fun findReadingRecordsByDynamicCondition( + userBookId: UUID, + sort: ReadingRecordSortType?, + pageable: Pageable + ): Page +} diff --git a/domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordSortType.kt b/domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordSortType.kt new file mode 100644 index 00000000..e2a700af --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordSortType.kt @@ -0,0 +1,8 @@ +package org.yapp.domain.readingrecord + +enum class ReadingRecordSortType { + CREATED_DATE_ASC, + CREATED_DATE_DESC, + PAGE_NUMBER_ASC, + PAGE_NUMBER_DESC; +} diff --git a/domain/src/main/kotlin/org/yapp/domain/readingrecord/vo/ReadingRecordInfoVO.kt b/domain/src/main/kotlin/org/yapp/domain/readingrecord/vo/ReadingRecordInfoVO.kt new file mode 100644 index 00000000..649f3549 --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/readingrecord/vo/ReadingRecordInfoVO.kt @@ -0,0 +1,40 @@ +package org.yapp.domain.readingrecord.vo + +import org.yapp.domain.readingrecord.ReadingRecord +import java.time.LocalDateTime + +data class ReadingRecordInfoVO private constructor( + val id: ReadingRecord.Id, + val userBookId: ReadingRecord.UserBookId, + val pageNumber: ReadingRecord.PageNumber, + val quote: ReadingRecord.Quote, + val review: ReadingRecord.Review, + val emotionTags: List, + val createdAt: LocalDateTime, + val updatedAt: LocalDateTime +) { + init { + require(emotionTags.size <= 3) { "Maximum 3 emotion tags are allowed" } + require(!createdAt.isAfter(updatedAt)) { + "생성일(createdAt)은 수정일(updatedAt)보다 이후일 수 없습니다." + } + } + + companion object { + fun newInstance( + readingRecord: ReadingRecord, + emotionTags: List + ): ReadingRecordInfoVO { + return ReadingRecordInfoVO( + id = readingRecord.id, + userBookId = readingRecord.userBookId, + pageNumber = readingRecord.pageNumber, + quote = readingRecord.quote, + review = readingRecord.review, + emotionTags = emotionTags, + createdAt = readingRecord.createdAt ?: throw IllegalStateException("createdAt은 null일 수 없습니다."), + updatedAt = readingRecord.updatedAt ?: throw IllegalStateException("updatedAt은 null일 수 없습니다.") + ) + } + } +} diff --git a/domain/src/main/kotlin/org/yapp/domain/readingrecordtag/ReadingRecordTag.kt b/domain/src/main/kotlin/org/yapp/domain/readingrecordtag/ReadingRecordTag.kt new file mode 100644 index 00000000..814cc476 --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/readingrecordtag/ReadingRecordTag.kt @@ -0,0 +1,51 @@ +package org.yapp.domain.readingrecordtag + +import org.yapp.domain.readingrecord.ReadingRecord +import org.yapp.domain.tag.Tag +import org.yapp.globalutils.util.UuidGenerator +import java.time.LocalDateTime +import java.util.UUID + +data class ReadingRecordTag private constructor( + val id: Id, + val readingRecordId: ReadingRecord.Id, + val tagId: Tag.Id, + val createdAt: LocalDateTime? = null, + val updatedAt: LocalDateTime? = null, + val deletedAt: LocalDateTime? = null, +) { + companion object { + fun create(readingRecordId: UUID, tagId: UUID): ReadingRecordTag { + return ReadingRecordTag( + id = Id.newInstance(UuidGenerator.create()), + readingRecordId = ReadingRecord.Id.newInstance(readingRecordId), + tagId = Tag.Id.newInstance(tagId) + ) + } + + fun reconstruct( + id: Id, + readingRecordId: ReadingRecord.Id, + tagId: Tag.Id, + createdAt: LocalDateTime? = null, + updatedAt: LocalDateTime? = null, + deletedAt: LocalDateTime? = null + ): ReadingRecordTag { + return ReadingRecordTag( + id = id, + readingRecordId = readingRecordId, + tagId = tagId, + createdAt = createdAt, + updatedAt = updatedAt, + deletedAt = deletedAt + ) + } + } + + @JvmInline + value class Id(val value: UUID) { + companion object { + fun newInstance(value: UUID) = Id(value) + } + } +} \ No newline at end of file diff --git a/domain/src/main/kotlin/org/yapp/domain/readingrecordtag/ReadingRecordTagRepository.kt b/domain/src/main/kotlin/org/yapp/domain/readingrecordtag/ReadingRecordTagRepository.kt new file mode 100644 index 00000000..0adf7682 --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/readingrecordtag/ReadingRecordTagRepository.kt @@ -0,0 +1,8 @@ +package org.yapp.domain.readingrecordtag + +import java.util.UUID + +interface ReadingRecordTagRepository { + fun saveAll(readingRecordTags: List): List + fun findByReadingRecordId(readingRecordId: UUID): List +} diff --git a/domain/src/main/kotlin/org/yapp/domain/tag/Tag.kt b/domain/src/main/kotlin/org/yapp/domain/tag/Tag.kt new file mode 100644 index 00000000..04b902b3 --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/tag/Tag.kt @@ -0,0 +1,45 @@ +package org.yapp.domain.tag + +import org.yapp.globalutils.util.UuidGenerator +import java.time.LocalDateTime +import java.util.UUID + +data class Tag private constructor( + val id: Id, + val name: String, + val createdAt: LocalDateTime? = null, + val updatedAt: LocalDateTime? = null, + val deletedAt: LocalDateTime? = null, +) { + companion object { + fun create(name: String): Tag { + return Tag( + id = Id.newInstance(UuidGenerator.create()), + name = name + ) + } + + fun reconstruct( + id: Id, + name: String, + createdAt: LocalDateTime? = null, + updatedAt: LocalDateTime? = null, + deletedAt: LocalDateTime? = null + ): Tag { + return Tag( + id = id, + name = name, + createdAt = createdAt, + updatedAt = updatedAt, + deletedAt = deletedAt + ) + } + } + + @JvmInline + value class Id(val value: UUID) { + companion object { + fun newInstance(value: UUID) = Id(value) + } + } +} \ No newline at end of file diff --git a/domain/src/main/kotlin/org/yapp/domain/tag/TagRepository.kt b/domain/src/main/kotlin/org/yapp/domain/tag/TagRepository.kt new file mode 100644 index 00000000..5eff7482 --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/tag/TagRepository.kt @@ -0,0 +1,10 @@ +package org.yapp.domain.tag + +import java.util.UUID + +interface TagRepository { + fun findByName(name: String): Tag? + fun save(tag: Tag): Tag + fun findByNames(names: List): List + fun findByIds(ids: List): List +} 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 18508944..4cb233ef 100644 --- a/domain/src/main/kotlin/org/yapp/domain/userbook/UserBook.kt +++ b/domain/src/main/kotlin/org/yapp/domain/userbook/UserBook.kt @@ -8,6 +8,7 @@ import java.util.* data class UserBook private constructor( val id: Id, val userId: UserId, + val bookId: BookId, val bookIsbn: BookIsbn, val coverImageUrl: String, val publisher: String, @@ -25,6 +26,7 @@ data class UserBook private constructor( companion object { fun create( userId: UUID, + bookId: UUID, bookIsbn: String, coverImageUrl: String, publisher: String, @@ -35,6 +37,7 @@ data class UserBook private constructor( return UserBook( id = Id.newInstance(UuidGenerator.create()), userId = UserId.newInstance(userId), + bookId = BookId.newInstance(bookId), bookIsbn = BookIsbn.newInstance(bookIsbn), coverImageUrl = coverImageUrl, publisher = publisher, @@ -47,6 +50,7 @@ data class UserBook private constructor( fun reconstruct( id: Id, userId: UserId, + bookId: BookId, bookIsbn: BookIsbn, coverImageUrl: String, publisher: String, @@ -60,6 +64,7 @@ data class UserBook private constructor( return UserBook( id = id, userId = userId, + bookId = bookId, bookIsbn = bookIsbn, coverImageUrl = coverImageUrl, publisher = publisher, @@ -87,6 +92,13 @@ data class UserBook private constructor( } } + @JvmInline + value class BookId(val value: UUID) { + companion object { + fun newInstance(value: UUID) = BookId(value) + } + } + @JvmInline value class BookIsbn(val value: String) { companion object { 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 1d1df0f5..5d54a4c6 100644 --- a/domain/src/main/kotlin/org/yapp/domain/userbook/UserBookDomainService.kt +++ b/domain/src/main/kotlin/org/yapp/domain/userbook/UserBookDomainService.kt @@ -13,6 +13,7 @@ class UserBookDomainService( ) { fun upsertUserBook( userId: UUID, + bookId: UUID, bookIsbn: String, bookTitle: String, bookAuthor: String, @@ -23,6 +24,7 @@ class UserBookDomainService( val userBook = userBookRepository.findByUserIdAndBookIsbn(userId, bookIsbn)?.updateStatus(status) ?: UserBook.create( userId = userId, + bookId = bookId, bookIsbn = bookIsbn, title = bookTitle, author = bookAuthor, @@ -35,15 +37,10 @@ class UserBookDomainService( return UserBookInfoVO.newInstance(savedUserBook) } - fun findAllUserBooks(userId: UUID): List { - val userBooks = userBookRepository.findAllByUserId(userId) - return userBooks.map { UserBookInfoVO.newInstance(it) } - } - fun findUserBooksByDynamicCondition( userId: UUID, status: BookStatus?, - sort: String?, + sort: UserBookSortType?, pageable: Pageable ): Page { val page = userBookRepository.findUserBooksByDynamicCondition(userId, status, sort, pageable) @@ -68,4 +65,8 @@ class UserBookDomainService( private fun countUserBooksByStatus(userId: UUID, status: BookStatus): Long { return userBookRepository.countUserBooksByStatus(userId, status) } + + fun findByIdAndUserId(userBookId: UUID, userId: UUID): UserBook? { + return userBookRepository.findByIdAndUserId(userBookId, userId) + } } 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 e6708655..b89b51d2 100644 --- a/domain/src/main/kotlin/org/yapp/domain/userbook/UserBookRepository.kt +++ b/domain/src/main/kotlin/org/yapp/domain/userbook/UserBookRepository.kt @@ -8,6 +8,8 @@ import java.util.UUID interface UserBookRepository { fun findByUserIdAndBookIsbn(userId: UUID, isbn: String): UserBook? + fun findByBookIdAndUserId(bookId: UUID, userId: UUID): UserBook? + fun findByIdAndUserId(id: UUID, userId: UUID): UserBook? fun save(userBook: UserBook): UserBook @@ -18,7 +20,7 @@ interface UserBookRepository { fun findUserBooksByDynamicCondition( userId: UUID, status: BookStatus?, - sort: String?, + sort: UserBookSortType?, pageable: Pageable ): Page diff --git a/domain/src/main/kotlin/org/yapp/domain/userbook/UserBookSortType.kt b/domain/src/main/kotlin/org/yapp/domain/userbook/UserBookSortType.kt new file mode 100644 index 00000000..93b290d4 --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/userbook/UserBookSortType.kt @@ -0,0 +1,9 @@ +package org.yapp.domain.userbook + + +enum class UserBookSortType { + TITLE_ASC, + TITLE_DESC, + CREATED_DATE_ASC, + CREATED_DATE_DESC; +} diff --git a/domain/src/main/kotlin/org/yapp/domain/userbook/vo/UserBookInfoVO.kt b/domain/src/main/kotlin/org/yapp/domain/userbook/vo/UserBookInfoVO.kt index 27cfbb03..2ec08712 100644 --- a/domain/src/main/kotlin/org/yapp/domain/userbook/vo/UserBookInfoVO.kt +++ b/domain/src/main/kotlin/org/yapp/domain/userbook/vo/UserBookInfoVO.kt @@ -7,6 +7,7 @@ import java.time.LocalDateTime data class UserBookInfoVO private constructor( val id: UserBook.Id, val userId: UserBook.UserId, + val bookId: UserBook.BookId, val bookIsbn: UserBook.BookIsbn, val coverImageUrl: String, val publisher: String, @@ -21,7 +22,7 @@ data class UserBookInfoVO private constructor( require(publisher.isNotBlank()) { "출판사는 비어 있을 수 없습니다." } require(title.isNotBlank()) { "도서 제목은 비어 있을 수 없습니다." } require(author.isNotBlank()) { "저자는 비어 있을 수 없습니다." } - require(createdAt.isBefore(updatedAt) || createdAt == updatedAt) { + require(!createdAt.isAfter(updatedAt)) { "생성일(createdAt)은 수정일(updatedAt)보다 이후일 수 없습니다." } } @@ -33,6 +34,7 @@ data class UserBookInfoVO private constructor( return UserBookInfoVO( id = userBook.id, userId = userBook.userId, + bookId = userBook.bookId, bookIsbn = userBook.bookIsbn, coverImageUrl = userBook.coverImageUrl, publisher = userBook.publisher, diff --git a/infra/src/main/kotlin/org/yapp/infra/book/entity/BookEntity.kt b/infra/src/main/kotlin/org/yapp/infra/book/entity/BookEntity.kt index 0282ca8c..382dc14a 100644 --- a/infra/src/main/kotlin/org/yapp/infra/book/entity/BookEntity.kt +++ b/infra/src/main/kotlin/org/yapp/infra/book/entity/BookEntity.kt @@ -6,17 +6,23 @@ import jakarta.persistence.Id import jakarta.persistence.Table import org.hibernate.annotations.JdbcTypeCode import org.hibernate.annotations.SQLDelete +import org.hibernate.annotations.SQLRestriction import org.yapp.domain.book.Book import org.yapp.infra.common.BaseTimeEntity import java.sql.Types +import java.util.UUID @Entity @Table(name = "books") @SQLDelete(sql = "UPDATE books SET deleted_at = NOW() WHERE isbn = ?") +@SQLRestriction("deleted_at IS NULL") class BookEntity private constructor( @Id @JdbcTypeCode(Types.VARCHAR) - @Column(length = 13, updatable = false, nullable = false) + @Column(length = 36, updatable = false, nullable = false) + val id: UUID, + + @Column(length = 13, updatable = false, nullable = false, unique = true) val isbn: String, title: String, @@ -24,8 +30,7 @@ class BookEntity private constructor( publisher: String, publicationYear: Int? = null, coverImageUrl: String, - description: String? = null - + description: String? = null, ) : BaseTimeEntity() { @Column(nullable = false, length = 255) @@ -52,7 +57,9 @@ class BookEntity private constructor( var description: String? = description protected set + fun toDomain(): Book = Book.reconstruct( + id = Book.Id.newInstance(this.id), isbn = Book.Isbn.newInstance(this.isbn), title = title, author = author, @@ -67,21 +74,22 @@ class BookEntity private constructor( companion object { fun fromDomain(book: Book): BookEntity = BookEntity( + id = book.id.value, isbn = book.isbn.value, title = book.title, author = book.author, publisher = book.publisher, publicationYear = book.publicationYear, coverImageUrl = book.coverImageUrl, - description = book.description + description = book.description, ) } override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is BookEntity) return false - return isbn == other.isbn + return id == other.id } - override fun hashCode(): Int = isbn.hashCode() + override fun hashCode(): Int = id.hashCode() } diff --git a/infra/src/main/kotlin/org/yapp/infra/book/repository/JpaBookRepository.kt b/infra/src/main/kotlin/org/yapp/infra/book/repository/JpaBookRepository.kt index f8fe92ad..25006647 100644 --- a/infra/src/main/kotlin/org/yapp/infra/book/repository/JpaBookRepository.kt +++ b/infra/src/main/kotlin/org/yapp/infra/book/repository/JpaBookRepository.kt @@ -6,5 +6,9 @@ import org.yapp.infra.book.entity.BookEntity /** * JPA repository for BookEntity. */ -interface JpaBookRepository : JpaRepository { +import java.util.UUID + +interface JpaBookRepository : JpaRepository { + fun findByIsbn(isbn: String): BookEntity? + fun existsByIsbn(isbn: String): Boolean } diff --git a/infra/src/main/kotlin/org/yapp/infra/book/repository/impl/BookRepositoryImpl.kt b/infra/src/main/kotlin/org/yapp/infra/book/repository/impl/BookRepositoryImpl.kt index dae5cbe4..a4c69541 100644 --- a/infra/src/main/kotlin/org/yapp/infra/book/repository/impl/BookRepositoryImpl.kt +++ b/infra/src/main/kotlin/org/yapp/infra/book/repository/impl/BookRepositoryImpl.kt @@ -6,19 +6,28 @@ import org.yapp.domain.book.Book import org.yapp.domain.book.BookRepository import org.yapp.infra.book.entity.BookEntity import org.yapp.infra.book.repository.JpaBookRepository +import java.util.UUID @Repository class BookRepositoryImpl( private val jpaBookRepository: JpaBookRepository ) : BookRepository { - override fun findById(id: String): Book? { + override fun findById(id: UUID): Book? { return jpaBookRepository.findByIdOrNull(id)?.toDomain() } - override fun existsById(id: String): Boolean { + override fun existsById(id: UUID): Boolean { return jpaBookRepository.existsById(id) } + override fun findByIsbn(isbn: String): Book? { + return jpaBookRepository.findByIsbn(isbn)?.toDomain() + } + + override fun existsByIsbn(isbn: String): Boolean { + return jpaBookRepository.existsByIsbn(isbn) + } + override fun save(book: Book): Book { val savedEntity = jpaBookRepository.saveAndFlush(BookEntity.fromDomain(book)) return savedEntity.toDomain() 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 index 0603c34e..56e7975c 100644 --- 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 @@ -20,7 +20,8 @@ data class BookItem internal constructor( @JsonProperty("cover") val cover: String?, @JsonProperty("categoryId") val categoryId: Int?, @JsonProperty("categoryName") val categoryName: String?, - @JsonProperty("publisher") val publisher: String? + @JsonProperty("publisher") val publisher: String?, + @JsonProperty("itemPage") val itemPage: Int? ) data class AladinSearchResponse internal constructor( diff --git a/infra/src/main/kotlin/org/yapp/infra/readingrecord/entity/ReadingRecordEntity.kt b/infra/src/main/kotlin/org/yapp/infra/readingrecord/entity/ReadingRecordEntity.kt new file mode 100644 index 00000000..fd18192e --- /dev/null +++ b/infra/src/main/kotlin/org/yapp/infra/readingrecord/entity/ReadingRecordEntity.kt @@ -0,0 +1,78 @@ +package org.yapp.infra.readingrecord.entity + +import jakarta.persistence.* +import org.hibernate.annotations.JdbcTypeCode +import org.hibernate.annotations.SQLDelete +import org.hibernate.annotations.SQLRestriction +import org.yapp.domain.readingrecord.ReadingRecord +import org.yapp.infra.common.BaseTimeEntity +import java.sql.Types +import java.util.* + +@Entity +@Table(name = "reading_records") +@SQLDelete(sql = "UPDATE reading_records SET deleted_at = NOW() WHERE id = ?") +@SQLRestriction("deleted_at IS NULL") +class ReadingRecordEntity( + @Id + @JdbcTypeCode(Types.VARCHAR) + @Column(length = 36, updatable = false, nullable = false) + val id: UUID, + + @Column(name = "user_book_id", nullable = false, length = 36) + @JdbcTypeCode(Types.VARCHAR) + val userBookId: UUID, + + pageNumber: Int, + quote: String, + review: String, + + +) : BaseTimeEntity() { + + @Column(name = "page_number", nullable = false) + var pageNumber: Int = pageNumber + protected set + + @Column(name = "quote", nullable = false, length = 1000) + var quote: String = quote + protected set + + @Column(name = "review", nullable = false, length = 1000) + var review: String = review + protected set + + fun toDomain(): ReadingRecord { + return ReadingRecord.reconstruct( + id = ReadingRecord.Id.newInstance(this.id), + userBookId = ReadingRecord.UserBookId.newInstance(this.userBookId), + pageNumber = ReadingRecord.PageNumber.newInstance(this.pageNumber), + quote = ReadingRecord.Quote.newInstance(this.quote), + review = ReadingRecord.Review.newInstance(this.review), + emotionTags = emptyList(), + createdAt = this.createdAt, + updatedAt = this.updatedAt, + deletedAt = this.deletedAt + ) + } + + companion object { + fun fromDomain(readingRecord: ReadingRecord): ReadingRecordEntity { + return ReadingRecordEntity( + id = readingRecord.id.value, + userBookId = readingRecord.userBookId.value, + pageNumber = readingRecord.pageNumber.value, + quote = readingRecord.quote.value, + review = readingRecord.review.value + ) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ReadingRecordEntity) return false + return id == other.id + } + + override fun hashCode(): Int = id.hashCode() +} diff --git a/infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/JpaReadingRecordQuerydslRepository.kt b/infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/JpaReadingRecordQuerydslRepository.kt new file mode 100644 index 00000000..5fbbd7e5 --- /dev/null +++ b/infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/JpaReadingRecordQuerydslRepository.kt @@ -0,0 +1,16 @@ +package org.yapp.infra.readingrecord.repository + +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.yapp.domain.readingrecord.ReadingRecordSortType +import org.yapp.infra.readingrecord.entity.ReadingRecordEntity +import java.util.UUID + +interface JpaReadingRecordQuerydslRepository { + + fun findReadingRecordsByDynamicCondition( + userBookId: UUID, + sort: ReadingRecordSortType?, + pageable: Pageable + ): Page +} diff --git a/infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/JpaReadingRecordRepository.kt b/infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/JpaReadingRecordRepository.kt new file mode 100644 index 00000000..84f43f5d --- /dev/null +++ b/infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/JpaReadingRecordRepository.kt @@ -0,0 +1,19 @@ +package org.yapp.infra.readingrecord.repository + +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.jpa.repository.JpaRepository +import org.yapp.infra.readingrecord.entity.ReadingRecordEntity +import java.util.UUID + + +interface JpaReadingRecordRepository : JpaRepository, JpaReadingRecordQuerydslRepository { + + fun findAllByUserBookId(userBookId: UUID): List + + fun findAllByUserBookId(userBookId: UUID, pageable: Pageable): Page + + fun findAllByUserBookIdIn(userBookIds: List): List + + fun countByUserBookId(userBookId: UUID): Long +} diff --git a/infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/JpaReadingRecordQuerydslRepositoryImpl.kt b/infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/JpaReadingRecordQuerydslRepositoryImpl.kt new file mode 100644 index 00000000..c4c827c2 --- /dev/null +++ b/infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/JpaReadingRecordQuerydslRepositoryImpl.kt @@ -0,0 +1,57 @@ +package org.yapp.infra.readingrecord.repository.impl + +import com.querydsl.core.types.OrderSpecifier +import com.querydsl.jpa.impl.JPAQueryFactory +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.Pageable +import org.springframework.stereotype.Repository +import org.yapp.domain.readingrecord.ReadingRecordSortType +import org.yapp.infra.readingrecord.entity.QReadingRecordEntity +import org.yapp.infra.readingrecord.entity.ReadingRecordEntity +import org.yapp.infra.readingrecord.repository.JpaReadingRecordQuerydslRepository +import java.util.* + +@Repository +class JpaReadingRecordQuerydslRepositoryImpl( + private val queryFactory: JPAQueryFactory +) : JpaReadingRecordQuerydslRepository { + + private val readingRecord = QReadingRecordEntity.readingRecordEntity + + override fun findReadingRecordsByDynamicCondition( + userBookId: UUID, + sort: ReadingRecordSortType?, + pageable: Pageable + ): Page { + + val whereCondition = readingRecord.userBookId.eq(userBookId) + + val results = queryFactory + .selectFrom(readingRecord) + .where(whereCondition) + .orderBy(createOrderSpecifier(sort)) + .offset(pageable.offset) + .limit(pageable.pageSize.toLong()) + .fetch() + + val total = queryFactory + .select(readingRecord.count()) + .from(readingRecord) + .where(whereCondition) + .fetchOne() ?: 0L + + return PageImpl(results, pageable, total) + } + + private fun createOrderSpecifier(sort: ReadingRecordSortType?): OrderSpecifier<*> { + return when (sort) { + ReadingRecordSortType.PAGE_NUMBER_ASC -> readingRecord.pageNumber.asc() + ReadingRecordSortType.PAGE_NUMBER_DESC -> readingRecord.pageNumber.desc() + ReadingRecordSortType.CREATED_DATE_ASC -> readingRecord.createdAt.asc() + ReadingRecordSortType.CREATED_DATE_DESC -> readingRecord.createdAt.desc() + null -> readingRecord.createdAt.desc() + } + } + +} 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 new file mode 100644 index 00000000..fe87c118 --- /dev/null +++ b/infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/ReadingRecordRepositoryImpl.kt @@ -0,0 +1,56 @@ +package org.yapp.infra.readingrecord.repository.impl + +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.repository.findByIdOrNull +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 + +@Repository +class ReadingRecordRepositoryImpl( + private val jpaReadingRecordRepository: JpaReadingRecordRepository +) : ReadingRecordRepository { + + override fun save(readingRecord: ReadingRecord): ReadingRecord { + val savedEntity = jpaReadingRecordRepository.saveAndFlush(ReadingRecordEntity.fromDomain(readingRecord)) + return savedEntity.toDomain() + } + + override fun findById(id: UUID): ReadingRecord? { + return jpaReadingRecordRepository.findByIdOrNull(id)?.toDomain() + } + + override fun findAllByUserBookId(userBookId: UUID): List { + val entities = jpaReadingRecordRepository.findAllByUserBookId(userBookId) + return entities.map { it.toDomain() } + } + + override fun findAllByUserBookId(userBookId: UUID, pageable: Pageable): Page { + val page = jpaReadingRecordRepository.findAllByUserBookId(userBookId, pageable) + return page.map { it.toDomain() } + } + + override fun findAllByUserBookIdIn(userBookIds: List): List { + val entities = jpaReadingRecordRepository.findAllByUserBookIdIn(userBookIds) + return entities.map { it.toDomain() } + } + + override fun countByUserBookId(userBookId: UUID): Long { + return jpaReadingRecordRepository.countByUserBookId(userBookId) + } + + override fun findReadingRecordsByDynamicCondition( + userBookId: UUID, + sort: ReadingRecordSortType?, + pageable: Pageable + ): Page { + val page = jpaReadingRecordRepository.findReadingRecordsByDynamicCondition(userBookId, sort, pageable) + return page.map { it.toDomain() } + } +} diff --git a/infra/src/main/kotlin/org/yapp/infra/readingrecordtag/entity/ReadingRecordTagEntity.kt b/infra/src/main/kotlin/org/yapp/infra/readingrecordtag/entity/ReadingRecordTagEntity.kt new file mode 100644 index 00000000..07e755b8 --- /dev/null +++ b/infra/src/main/kotlin/org/yapp/infra/readingrecordtag/entity/ReadingRecordTagEntity.kt @@ -0,0 +1,62 @@ +package org.yapp.infra.readingrecordtag.entity + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.Id +import jakarta.persistence.Table +import org.hibernate.annotations.JdbcTypeCode +import org.hibernate.annotations.SQLDelete +import org.hibernate.annotations.SQLRestriction +import org.yapp.domain.readingrecordtag.ReadingRecordTag +import org.yapp.infra.common.BaseTimeEntity +import java.sql.Types +import java.util.UUID + +@Entity +@Table(name = "reading_record_tags") +@SQLDelete(sql = "UPDATE reading_record_tags SET deleted_at = NOW() WHERE id = ?") +@SQLRestriction("deleted_at IS NULL") +class ReadingRecordTagEntity( + @Id + @JdbcTypeCode(Types.VARCHAR) + @Column(length = 36, updatable = false, nullable = false) + val id: UUID, + + @Column(name = "reading_record_id", nullable = false, length = 36) + @JdbcTypeCode(Types.VARCHAR) + val readingRecordId: UUID, + + @Column(name = "tag_id", nullable = false, length = 36) + @JdbcTypeCode(Types.VARCHAR) + val tagId: UUID +) : BaseTimeEntity() { + + fun toDomain(): ReadingRecordTag { + return ReadingRecordTag.reconstruct( + id = ReadingRecordTag.Id.newInstance(this.id), + readingRecordId = org.yapp.domain.readingrecord.ReadingRecord.Id.newInstance(this.readingRecordId), + tagId = org.yapp.domain.tag.Tag.Id.newInstance(this.tagId), + createdAt = this.createdAt, + updatedAt = this.updatedAt, + deletedAt = this.deletedAt + ) + } + + companion object { + fun fromDomain(readingRecordTag: ReadingRecordTag): ReadingRecordTagEntity { + return ReadingRecordTagEntity( + id = readingRecordTag.id.value, + readingRecordId = readingRecordTag.readingRecordId.value, + tagId = readingRecordTag.tagId.value + ) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ReadingRecordTagEntity) return false + return id == other.id + } + + override fun hashCode(): Int = id.hashCode() +} \ No newline at end of file diff --git a/infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/JpaReadingRecordTagRepository.kt b/infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/JpaReadingRecordTagRepository.kt new file mode 100644 index 00000000..18045acd --- /dev/null +++ b/infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/JpaReadingRecordTagRepository.kt @@ -0,0 +1,9 @@ +package org.yapp.infra.readingrecordtag.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.yapp.infra.readingrecordtag.entity.ReadingRecordTagEntity +import java.util.UUID + +interface JpaReadingRecordTagRepository : JpaRepository { + fun findByReadingRecordId(readingRecordId: UUID): List +} diff --git a/infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/ReadingRecordTagRepositoryImpl.kt b/infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/ReadingRecordTagRepositoryImpl.kt new file mode 100644 index 00000000..58e29896 --- /dev/null +++ b/infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/ReadingRecordTagRepositoryImpl.kt @@ -0,0 +1,20 @@ +package org.yapp.infra.readingrecordtag.repository + +import org.springframework.stereotype.Repository +import org.yapp.domain.readingrecordtag.ReadingRecordTag +import org.yapp.domain.readingrecordtag.ReadingRecordTagRepository +import org.yapp.infra.readingrecordtag.entity.ReadingRecordTagEntity + +@Repository +class ReadingRecordTagRepositoryImpl( + private val jpaReadingRecordTagRepository: JpaReadingRecordTagRepository +) : ReadingRecordTagRepository { + override fun saveAll(readingRecordTags: List): List { + val entities = readingRecordTags.map { ReadingRecordTagEntity.fromDomain(it) } + return jpaReadingRecordTagRepository.saveAll(entities).map { it.toDomain() } + } + + override fun findByReadingRecordId(readingRecordId: java.util.UUID): List { + return jpaReadingRecordTagRepository.findByReadingRecordId(readingRecordId).map { it.toDomain() } + } +} \ No newline at end of file diff --git a/infra/src/main/kotlin/org/yapp/infra/tag/entity/TagEntity.kt b/infra/src/main/kotlin/org/yapp/infra/tag/entity/TagEntity.kt new file mode 100644 index 00000000..1dab69cf --- /dev/null +++ b/infra/src/main/kotlin/org/yapp/infra/tag/entity/TagEntity.kt @@ -0,0 +1,55 @@ +package org.yapp.infra.tag.entity + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.Id +import jakarta.persistence.Table +import org.hibernate.annotations.JdbcTypeCode +import org.hibernate.annotations.SQLDelete +import org.hibernate.annotations.SQLRestriction +import org.yapp.domain.tag.Tag +import org.yapp.infra.common.BaseTimeEntity +import java.sql.Types +import java.util.UUID + +@Entity +@Table(name = "tags") +@SQLDelete(sql = "UPDATE tags SET deleted_at = NOW() WHERE id = ?") +@SQLRestriction("deleted_at IS NULL") +class TagEntity( + @Id + @JdbcTypeCode(Types.VARCHAR) + @Column(length = 36, updatable = false, nullable = false) + val id: UUID, + + @Column(nullable = false, length = 10, unique = true) + val name: String +) : BaseTimeEntity() { + + fun toDomain(): Tag { + return Tag.reconstruct( + id = Tag.Id.newInstance(this.id), + name = this.name, + createdAt = this.createdAt, + updatedAt = this.updatedAt, + deletedAt = this.deletedAt + ) + } + + companion object { + fun fromDomain(tag: Tag): TagEntity { + return TagEntity( + id = tag.id.value, + name = tag.name + ) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is TagEntity) return false + return id == other.id + } + + override fun hashCode(): Int = id.hashCode() +} \ No newline at end of file diff --git a/infra/src/main/kotlin/org/yapp/infra/tag/repository/JpaTagRepository.kt b/infra/src/main/kotlin/org/yapp/infra/tag/repository/JpaTagRepository.kt new file mode 100644 index 00000000..fb259f54 --- /dev/null +++ b/infra/src/main/kotlin/org/yapp/infra/tag/repository/JpaTagRepository.kt @@ -0,0 +1,11 @@ +package org.yapp.infra.tag.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.yapp.infra.tag.entity.TagEntity +import java.util.UUID + +interface JpaTagRepository : JpaRepository { + fun findByName(name: String): TagEntity? + fun findByNameIn(names: List): List + fun findByIdIn(ids: List): List +} diff --git a/infra/src/main/kotlin/org/yapp/infra/tag/repository/TagRepositoryImpl.kt b/infra/src/main/kotlin/org/yapp/infra/tag/repository/TagRepositoryImpl.kt new file mode 100644 index 00000000..832e56ca --- /dev/null +++ b/infra/src/main/kotlin/org/yapp/infra/tag/repository/TagRepositoryImpl.kt @@ -0,0 +1,28 @@ +package org.yapp.infra.tag.repository + +import org.springframework.stereotype.Repository +import org.yapp.domain.tag.Tag +import org.yapp.domain.tag.TagRepository +import org.yapp.infra.tag.entity.TagEntity +import java.util.UUID + +@Repository +class TagRepositoryImpl( + private val jpaTagRepository: JpaTagRepository +) : TagRepository { + override fun findByName(name: String): Tag? { + return jpaTagRepository.findByName(name)?.toDomain() + } + + override fun save(tag: Tag): Tag { + return jpaTagRepository.save(TagEntity.fromDomain(tag)).toDomain() + } + + override fun findByNames(names: List): List { + return jpaTagRepository.findByNameIn(names).map { it.toDomain() } + } + + override fun findByIds(ids: List): List { + return jpaTagRepository.findByIdIn(ids).map { it.toDomain() } + } +} \ No newline at end of file diff --git a/infra/src/main/kotlin/org/yapp/infra/userbook/entity/UserBookEntity.kt b/infra/src/main/kotlin/org/yapp/infra/userbook/entity/UserBookEntity.kt index 8f688aa4..1802ae03 100644 --- a/infra/src/main/kotlin/org/yapp/infra/userbook/entity/UserBookEntity.kt +++ b/infra/src/main/kotlin/org/yapp/infra/userbook/entity/UserBookEntity.kt @@ -3,6 +3,7 @@ package org.yapp.infra.userbook.entity import jakarta.persistence.* import org.hibernate.annotations.JdbcTypeCode import org.hibernate.annotations.SQLDelete +import org.hibernate.annotations.SQLRestriction import org.yapp.infra.common.BaseTimeEntity import org.yapp.domain.userbook.BookStatus import org.yapp.domain.userbook.UserBook @@ -12,6 +13,7 @@ import java.util.* @Entity @Table(name = "user_books") @SQLDelete(sql = "UPDATE user_books SET deleted_at = NOW() WHERE id = ?") +@SQLRestriction("deleted_at IS NULL") class UserBookEntity( @Id @JdbcTypeCode(Types.VARCHAR) @@ -22,6 +24,10 @@ class UserBookEntity( @JdbcTypeCode(Types.VARCHAR) val userId: UUID, + @Column(name = "book_id", nullable = false, length = 36) + @JdbcTypeCode(Types.VARCHAR) + val bookId: UUID, + @Column(name = "book_isbn", nullable = false) val bookIsbn: String, @@ -56,6 +62,7 @@ class UserBookEntity( fun toDomain(): UserBook = UserBook.reconstruct( id = UserBook.Id.newInstance(this.id), userId = UserBook.UserId.newInstance(this.userId), + bookId = UserBook.BookId.newInstance(this.bookId), bookIsbn = UserBook.BookIsbn.newInstance(this.bookIsbn), coverImageUrl = this.coverImageUrl, publisher = this.publisher, @@ -72,6 +79,7 @@ class UserBookEntity( return UserBookEntity( id = userBook.id.value, userId = userBook.userId.value, + bookId = userBook.bookId.value, bookIsbn = userBook.bookIsbn.value, coverImageUrl = userBook.coverImageUrl, publisher = userBook.publisher, diff --git a/infra/src/main/kotlin/org/yapp/infra/userbook/repository/JpaUserBookQuerydslRepository.kt b/infra/src/main/kotlin/org/yapp/infra/userbook/repository/JpaUserBookQuerydslRepository.kt index 5149427b..3f178a38 100644 --- a/infra/src/main/kotlin/org/yapp/infra/userbook/repository/JpaUserBookQuerydslRepository.kt +++ b/infra/src/main/kotlin/org/yapp/infra/userbook/repository/JpaUserBookQuerydslRepository.kt @@ -3,6 +3,7 @@ package org.yapp.infra.userbook.repository import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.yapp.domain.userbook.BookStatus +import org.yapp.domain.userbook.UserBookSortType import org.yapp.infra.userbook.entity.UserBookEntity import java.util.UUID @@ -10,7 +11,7 @@ interface JpaUserBookQuerydslRepository { fun findUserBooksByDynamicCondition( userId: UUID, status: BookStatus?, - sort: String?, + sort: UserBookSortType?, pageable: Pageable ): Page 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 e4a66dd5..cf631c40 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 @@ -6,6 +6,8 @@ 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 findAllByUserId(userId: UUID): List fun findAllByUserIdAndBookIsbnIn(userId: UUID, bookIsbnList: List): List diff --git a/infra/src/main/kotlin/org/yapp/infra/userbook/repository/impl/JpaUserBookQuerydslRepositoryImpl.kt b/infra/src/main/kotlin/org/yapp/infra/userbook/repository/impl/JpaUserBookQuerydslRepositoryImpl.kt index cda6184f..6c4a5f82 100644 --- a/infra/src/main/kotlin/org/yapp/infra/userbook/repository/impl/JpaUserBookQuerydslRepositoryImpl.kt +++ b/infra/src/main/kotlin/org/yapp/infra/userbook/repository/impl/JpaUserBookQuerydslRepositoryImpl.kt @@ -8,6 +8,7 @@ import org.springframework.data.domain.PageImpl import org.springframework.data.domain.Pageable import org.springframework.stereotype.Repository import org.yapp.domain.userbook.BookStatus +import org.yapp.domain.userbook.UserBookSortType import org.yapp.infra.userbook.entity.QUserBookEntity import org.yapp.infra.userbook.entity.UserBookEntity import org.yapp.infra.userbook.repository.JpaUserBookQuerydslRepository @@ -23,7 +24,7 @@ class JpaUserBookQuerydslRepositoryImpl( override fun findUserBooksByDynamicCondition( userId: UUID, status: BookStatus?, - sort: String?, + sort: UserBookSortType?, pageable: Pageable ): Page { val baseQuery = queryFactory @@ -40,13 +41,13 @@ class JpaUserBookQuerydslRepositoryImpl( .fetch() val total = queryFactory - .select(userBook.count()) - .from(userBook) - .where( - userBook.userId.eq(userId), - statusEq(status) - ) - .fetchOne() ?: 0L + .select(userBook.count()) + .from(userBook) + .where( + userBook.userId.eq(userId), + statusEq(status) + ) + .fetchOne() ?: 0L return PageImpl(results, pageable, total) } @@ -69,13 +70,13 @@ class JpaUserBookQuerydslRepositoryImpl( return status?.let { userBook.status.eq(it) } } - private fun createOrderSpecifier(sort: String?): OrderSpecifier<*> { + private fun createOrderSpecifier(sort: UserBookSortType?): OrderSpecifier<*> { return when (sort) { - "title_asc" -> userBook.title.asc() - "title_desc" -> userBook.title.desc() - "date_asc" -> userBook.createdAt.asc() - "date_desc" -> userBook.createdAt.desc() - else -> userBook.createdAt.desc() + UserBookSortType.TITLE_ASC -> userBook.title.asc() + UserBookSortType.TITLE_DESC -> userBook.title.desc() + UserBookSortType.CREATED_DATE_ASC -> userBook.createdAt.asc() + UserBookSortType.CREATED_DATE_DESC -> userBook.createdAt.desc() + null -> userBook.createdAt.desc() } } } 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 c93e3ce4..16cfaae0 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 @@ -6,6 +6,7 @@ import org.springframework.stereotype.Repository import org.yapp.domain.userbook.BookStatus import org.yapp.domain.userbook.UserBookRepository import org.yapp.domain.userbook.UserBook +import org.yapp.domain.userbook.UserBookSortType import org.yapp.infra.userbook.entity.UserBookEntity import org.yapp.infra.userbook.repository.JpaUserBookRepository import java.util.* @@ -17,7 +18,14 @@ class UserBookRepositoryImpl( override fun findByUserIdAndBookIsbn(userId: UUID, isbn: String): UserBook? { return jpaUserBookRepository.findByUserIdAndBookIsbn(userId, isbn)?.toDomain() + } + + override fun findByBookIdAndUserId(bookId: UUID, userId: UUID): UserBook? { + return jpaUserBookRepository.findByBookIdAndUserId(bookId, userId)?.toDomain() + } + override fun findByIdAndUserId(id: UUID, userId: UUID): UserBook? { + return jpaUserBookRepository.findByIdAndUserId(id, userId)?.toDomain() } override fun save(userBook: UserBook): UserBook { @@ -40,7 +48,7 @@ class UserBookRepositoryImpl( override fun findUserBooksByDynamicCondition( userId: UUID, status: BookStatus?, - sort: String?, + sort: UserBookSortType?, pageable: Pageable ): Page { return jpaUserBookRepository.findUserBooksByDynamicCondition(userId, status, sort, pageable)