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 67ead64d..e88075d9 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 @@ -8,17 +8,12 @@ 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.PutMapping -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.* import org.yapp.apis.book.dto.request.BookDetailRequest import org.yapp.apis.book.dto.request.BookSearchRequest import org.yapp.apis.book.dto.request.UserBookRegisterRequest @@ -29,7 +24,7 @@ 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 +import java.util.* @Tag(name = "Books", description = "도서 정보를 조회하는 API") @@ -146,5 +141,4 @@ interface BookControllerApi { @PageableDefault(size = 10, sort = ["createdAt"], direction = Sort.Direction.DESC) pageable: Pageable ): ResponseEntity - } 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 41814c34..b2742c46 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 @@ -4,17 +4,16 @@ import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.stereotype.Service import org.yapp.apis.auth.dto.request.UserBooksByIsbnsRequest +import org.yapp.apis.book.dto.request.UpsertUserBookRequest import org.yapp.apis.book.dto.response.UserBookPageResponse import org.yapp.apis.book.dto.response.UserBookResponse -import org.yapp.apis.book.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.UserBookSortType -import java.util.UUID - +import java.util.* @Service class UserBookService( @@ -42,7 +41,6 @@ class UserBookService( ) } - fun findAllByUserIdAndBookIsbnIn(userBooksByIsbnsRequest: UserBooksByIsbnsRequest): List { val userBooks = userBookDomainService.findAllByUserIdAndBookIsbnIn( userBooksByIsbnsRequest.validUserId(), @@ -51,6 +49,8 @@ class UserBookService( return userBooks.map { UserBookResponse.from(it) } } + + private fun findUserBooksByDynamicCondition( userId: UUID, status: BookStatus?, diff --git a/apis/src/main/kotlin/org/yapp/apis/home/controller/HomeController.kt b/apis/src/main/kotlin/org/yapp/apis/home/controller/HomeController.kt new file mode 100644 index 00000000..d2fe29b5 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/home/controller/HomeController.kt @@ -0,0 +1,31 @@ +package org.yapp.apis.home.controller + +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import org.springframework.http.ResponseEntity +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.GetMapping +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.home.dto.response.UserHomeResponse +import org.yapp.apis.home.usecase.HomeUseCase +import java.util.* + +@Validated +@RestController +@RequestMapping("/api/home") +class HomeController( + private val homeUseCase: HomeUseCase +) : HomeControllerApi { + + @GetMapping + override fun getUserHomeData( + @AuthenticationPrincipal userId: UUID, + @RequestParam(defaultValue = "3") @Min(1) @Max(10) limit: Int + ): ResponseEntity { + val homeData = homeUseCase.getUserHomeData(userId, limit) + return ResponseEntity.ok(homeData) + } +} diff --git a/apis/src/main/kotlin/org/yapp/apis/home/controller/HomeControllerApi.kt b/apis/src/main/kotlin/org/yapp/apis/home/controller/HomeControllerApi.kt new file mode 100644 index 00000000..d90c1556 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/home/controller/HomeControllerApi.kt @@ -0,0 +1,57 @@ +package org.yapp.apis.home.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.constraints.Max +import jakarta.validation.constraints.Min +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.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.yapp.apis.home.dto.response.UserHomeResponse +import org.yapp.globalutils.exception.ErrorResponse +import java.util.* + +@Tag(name = "Home", description = "홈 화면 관련 API") +@RequestMapping("/api/home") +interface HomeControllerApi { + + @Operation( + summary = "홈 화면 데이터 조회", + description = "사용자의 홈 화면에 필요한 데이터를 조회합니다. 요즘 읽는 책 목록을 우선순위에 따라 최대 limit개까지 반환합니다." + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "홈 화면 데이터 조회 성공", + content = [Content(schema = Schema(implementation = UserHomeResponse::class))] + ), + ApiResponse( + responseCode = "400", + description = "잘못된 요청", + content = [Content(schema = Schema(implementation = ErrorResponse::class))] + ), + ApiResponse( + responseCode = "404", + description = "사용자를 찾을 수 없음", + content = [Content(schema = Schema(implementation = ErrorResponse::class))] + ) + ] + ) + @GetMapping + fun getUserHomeData( + @AuthenticationPrincipal userId: UUID, + @Parameter( + description = "조회할 최대 도서 수 (기본값: 3, 최대: 10)", + example = "3" + ) + @RequestParam(defaultValue = "3") @Min(1) @Max(10) limit: Int + ): ResponseEntity +} diff --git a/apis/src/main/kotlin/org/yapp/apis/home/dto/response/UserHomeResponse.kt b/apis/src/main/kotlin/org/yapp/apis/home/dto/response/UserHomeResponse.kt new file mode 100644 index 00000000..dc80a5fe --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/home/dto/response/UserHomeResponse.kt @@ -0,0 +1,41 @@ +package org.yapp.apis.home.dto.response + +import org.yapp.domain.userbook.vo.HomeBookVO +import java.time.LocalDateTime +import java.util.* + +data class UserHomeResponse private constructor( + val recentBooks: List +) { + data class RecentBookResponse( + val userBookId: UUID, + val title: String, + val author: String, + val publisher: String, + val coverImageUrl: String, + val lastRecordedAt: LocalDateTime, + val recordCount: Int + ) { + companion object { + fun from(userBookInfo: HomeBookVO): RecentBookResponse { + return RecentBookResponse( + userBookId = userBookInfo.id.value, + title = userBookInfo.title, + author = userBookInfo.author, + publisher = userBookInfo.publisher, + coverImageUrl = userBookInfo.coverImageUrl, + lastRecordedAt = userBookInfo.lastRecordedAt, + recordCount = userBookInfo.recordCount + ) + } + } + } + + companion object { + fun from(recentBooks: List): UserHomeResponse { + return UserHomeResponse( + recentBooks = recentBooks.map { RecentBookResponse.from(it) } + ) + } + } +} diff --git a/apis/src/main/kotlin/org/yapp/apis/home/service/HomeService.kt b/apis/src/main/kotlin/org/yapp/apis/home/service/HomeService.kt new file mode 100644 index 00000000..aa98cbb7 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/home/service/HomeService.kt @@ -0,0 +1,36 @@ +package org.yapp.apis.home.service + +import org.springframework.stereotype.Service +import org.yapp.apis.home.dto.response.UserHomeResponse +import org.yapp.domain.userbook.UserBookDomainService +import org.yapp.domain.userbook.vo.HomeBookVO +import java.util.* + +@Service +class HomeService( + private val userBookDomainService: UserBookDomainService +) { + fun getUserHomeData(userId: UUID, limit: Int): UserHomeResponse { + val selectedBooks = selectBooksForHome(userId, limit) + return UserHomeResponse.from(selectedBooks) + } + + private fun selectBooksForHome(userId: UUID, limit: Int): List { + val booksWithRecords = userBookDomainService.findBooksWithRecordsOrderByLatest(userId) + + if (booksWithRecords.size >= limit) { + return booksWithRecords.take(limit) + } + + val neededCount = limit - booksWithRecords.size + val excludedBookIds = booksWithRecords.map { it.id.value }.toSet() + + val booksWithoutRecords = userBookDomainService.findBooksWithoutRecordsByStatusPriority( + userId, + neededCount, + excludedBookIds + ) + + return booksWithRecords + booksWithoutRecords + } +} \ No newline at end of file diff --git a/apis/src/main/kotlin/org/yapp/apis/home/usecase/HomeUseCase.kt b/apis/src/main/kotlin/org/yapp/apis/home/usecase/HomeUseCase.kt new file mode 100644 index 00000000..670f55b3 --- /dev/null +++ b/apis/src/main/kotlin/org/yapp/apis/home/usecase/HomeUseCase.kt @@ -0,0 +1,18 @@ +package org.yapp.apis.home.usecase + +import org.springframework.transaction.annotation.Transactional +import org.yapp.apis.home.service.HomeService +import org.yapp.apis.home.dto.response.* +import org.yapp.globalutils.annotation.UseCase +import java.util.* + +@UseCase +@Transactional(readOnly = true) +class HomeUseCase( + private val homeService: HomeService +) { + fun getUserHomeData(userId: UUID, limit: Int = 3): UserHomeResponse { + val validatedLimit = limit.coerceIn(1, 10) + return homeService.getUserHomeData(userId, validatedLimit) + } +} diff --git a/apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordService.kt b/apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordService.kt index 05c3af15..8d0633d0 100644 --- a/apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordService.kt +++ b/apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordService.kt @@ -6,17 +6,15 @@ 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 +import java.util.* @Service class ReadingRecordService( private val readingRecordDomainService: ReadingRecordDomainService, private val userBookService: UserBookService, - private val bookDomainService: BookDomainService ) { fun createReadingRecord( @@ -26,7 +24,6 @@ class ReadingRecordService( ): ReadingRecordResponse { userBookService.validateUserBookExists(userId, userBookId) - val readingRecordInfoVO = readingRecordDomainService.createReadingRecord( userBookId = userBookId, pageNumber = request.validPageNumber(), diff --git a/apis/src/main/resources/static/kakao-login.html b/apis/src/main/resources/static/kakao-login.html index 62819bc6..b3edd49b 100644 --- a/apis/src/main/resources/static/kakao-login.html +++ b/apis/src/main/resources/static/kakao-login.html @@ -121,7 +121,7 @@

소셜 로그인 테스트

fetch(`${API_SERVER}/api/v1/auth/signin`, { method: 'POST', headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({providerType: "KAKAO", accessToken}) + body: JSON.stringify({providerType: "KAKAO", oauthToken: accessToken}) }) .then(res => res.json()) .then(data => { diff --git a/domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecord.kt b/domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecord.kt index 5d85d999..9819d504 100644 --- a/domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecord.kt +++ b/domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecord.kt @@ -15,8 +15,6 @@ data class ReadingRecord private constructor( val updatedAt: LocalDateTime? = null, val deletedAt: LocalDateTime? = null, ) { - - companion object { fun create( userBookId: UUID, 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 72b89f2f..2928196b 100644 --- a/domain/src/main/kotlin/org/yapp/domain/userbook/UserBookDomainService.kt +++ b/domain/src/main/kotlin/org/yapp/domain/userbook/UserBookDomainService.kt @@ -2,10 +2,11 @@ package org.yapp.domain.userbook import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable +import org.yapp.domain.userbook.vo.HomeBookVO import org.yapp.domain.userbook.vo.UserBookInfoVO import org.yapp.domain.userbook.vo.UserBookStatusCountsVO import org.yapp.globalutils.annotation.DomainService -import java.util.UUID +import java.util.* @DomainService class UserBookDomainService( @@ -70,4 +71,36 @@ class UserBookDomainService( fun findByIdAndUserId(userBookId: UUID, userId: UUID): UserBook? { return userBookRepository.findByIdAndUserId(userBookId, userId) } + + fun findBooksWithRecordsOrderByLatest(userId: UUID): List { + val resultTriples = userBookRepository.findRecordedBooksSortedByRecency(userId) + + return resultTriples.map { (userBook, lastRecordedAt, recordCount) -> + HomeBookVO.newInstance( + userBook = userBook, + lastRecordedAt = lastRecordedAt, + recordCount = recordCount.toInt() + ) + } + } + + fun findBooksWithoutRecordsByStatusPriority( + userId: UUID, + limit: Int, + excludeIds: Set + ): List { + val userBooks = userBookRepository.findUnrecordedBooksSortedByPriority( + userId, + limit, + excludeIds + ) + + return userBooks.map { userBook -> + HomeBookVO.newInstance( + userBook = userBook, + lastRecordedAt = userBook.updatedAt ?: throw IllegalStateException("UserBook의 updatedAt이 null입니다: ${userBook.id}"), + recordCount = 0 + ) + } + } } 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 d8a1e9d9..56b2a9ac 100644 --- a/domain/src/main/kotlin/org/yapp/domain/userbook/UserBookRepository.kt +++ b/domain/src/main/kotlin/org/yapp/domain/userbook/UserBookRepository.kt @@ -2,7 +2,8 @@ package org.yapp.domain.userbook import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable -import java.util.UUID +import java.time.LocalDateTime +import java.util.* interface UserBookRepository { @@ -28,4 +29,11 @@ interface UserBookRepository { fun countUserBooksByStatus(userId: UUID, status: BookStatus): Long + fun findRecordedBooksSortedByRecency(userId: UUID): List> + + fun findUnrecordedBooksSortedByPriority( + userId: UUID, + limit: Int, + excludeIds: Set + ): List } diff --git a/domain/src/main/kotlin/org/yapp/domain/userbook/vo/HomeBookVO.kt b/domain/src/main/kotlin/org/yapp/domain/userbook/vo/HomeBookVO.kt new file mode 100644 index 00000000..8785a452 --- /dev/null +++ b/domain/src/main/kotlin/org/yapp/domain/userbook/vo/HomeBookVO.kt @@ -0,0 +1,56 @@ +package org.yapp.domain.userbook.vo + +import org.yapp.domain.userbook.BookStatus +import org.yapp.domain.userbook.UserBook +import java.time.LocalDateTime + +data class HomeBookVO private constructor( + val id: UserBook.Id, + val userId: UserBook.UserId, + val bookId: UserBook.BookId, + val bookIsbn: UserBook.BookIsbn, + val coverImageUrl: String, + val publisher: String, + val title: String, + val author: String, + val status: BookStatus, + val createdAt: LocalDateTime, + val updatedAt: LocalDateTime, + val lastRecordedAt: LocalDateTime, + val recordCount: Int +) { + init { + require(coverImageUrl.isNotBlank()) { "표지 이미지 URL은 비어 있을 수 없습니다." } + require(publisher.isNotBlank()) { "출판사는 비어 있을 수 없습니다." } + require(title.isNotBlank()) { "도서 제목은 비어 있을 수 없습니다." } + require(author.isNotBlank()) { "저자는 비어 있을 수 없습니다." } + require(!createdAt.isAfter(updatedAt)) { + "생성일(createdAt)은 수정일(updatedAt)보다 이후일 수 없습니다." + } + require(recordCount >= 0) { "독서 기록 수는 0 이상이어야 합니다." } + } + + companion object { + fun newInstance( + userBook: UserBook, + lastRecordedAt: LocalDateTime, + recordCount: Int + ): HomeBookVO { + return HomeBookVO( + id = userBook.id, + userId = userBook.userId, + bookId = userBook.bookId, + bookIsbn = userBook.bookIsbn, + coverImageUrl = userBook.coverImageUrl, + publisher = userBook.publisher, + title = userBook.title, + author = userBook.author, + status = userBook.status, + createdAt = userBook.createdAt ?: throw IllegalStateException("createdAt은 null일 수 없습니다."), + updatedAt = userBook.updatedAt ?: throw IllegalStateException("updatedAt은 null일 수 없습니다."), + lastRecordedAt = lastRecordedAt, + recordCount = recordCount + ) + } + } +} 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 943c7917..f79f9687 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 @@ -5,7 +5,8 @@ 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 +import org.yapp.infra.userbook.repository.dto.UserBookLastRecordProjection +import java.util.* interface JpaUserBookQuerydslRepository { fun findUserBooksByDynamicCondition( @@ -20,4 +21,12 @@ interface JpaUserBookQuerydslRepository { userId: UUID, status: BookStatus ): Long + + fun findRecordedBooksSortedByRecency(userId: UUID): List + + fun findUnrecordedBooksSortedByPriority( + userId: UUID, + excludeIds: Set, + limit: Int + ): List } 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 cf631c40..d6887977 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 @@ -10,5 +10,4 @@ interface JpaUserBookRepository : JpaRepository, JpaUserBo 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/dto/UserBookLastRecordProjection.kt b/infra/src/main/kotlin/org/yapp/infra/userbook/repository/dto/UserBookLastRecordProjection.kt new file mode 100644 index 00000000..f827cc1b --- /dev/null +++ b/infra/src/main/kotlin/org/yapp/infra/userbook/repository/dto/UserBookLastRecordProjection.kt @@ -0,0 +1,11 @@ +package org.yapp.infra.userbook.repository.dto + +import com.querydsl.core.annotations.QueryProjection +import org.yapp.infra.userbook.entity.UserBookEntity +import java.time.LocalDateTime + +data class UserBookLastRecordProjection @QueryProjection constructor( + val userBookEntity: UserBookEntity, + val lastRecordedAt: LocalDateTime, + val recordCount: Long +) 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 c035b0ad..ce487e6c 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 @@ -2,6 +2,8 @@ package org.yapp.infra.userbook.repository.impl import com.querydsl.core.types.OrderSpecifier import com.querydsl.core.types.dsl.BooleanExpression +import com.querydsl.core.types.dsl.CaseBuilder +import com.querydsl.jpa.JPAExpressions import com.querydsl.jpa.impl.JPAQueryFactory import org.springframework.data.domain.Page import org.springframework.data.domain.PageImpl @@ -9,9 +11,12 @@ 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.readingrecord.entity.QReadingRecordEntity import org.yapp.infra.userbook.entity.QUserBookEntity import org.yapp.infra.userbook.entity.UserBookEntity import org.yapp.infra.userbook.repository.JpaUserBookQuerydslRepository +import org.yapp.infra.userbook.repository.dto.QUserBookLastRecordProjection +import org.yapp.infra.userbook.repository.dto.UserBookLastRecordProjection import java.util.* @Repository @@ -20,6 +25,7 @@ class JpaUserBookQuerydslRepositoryImpl( ) : JpaUserBookQuerydslRepository { private val userBook = QUserBookEntity.userBookEntity + private val readingRecord = QReadingRecordEntity.readingRecordEntity override fun findUserBooksByDynamicCondition( userId: UUID, @@ -69,6 +75,79 @@ class JpaUserBookQuerydslRepositoryImpl( .fetchOne() ?: 0L } + override fun findRecordedBooksSortedByRecency(userId: UUID): List { + return queryFactory + .select( + QUserBookLastRecordProjection( + userBook, + readingRecord.updatedAt.max(), + readingRecord.count() + ) + ) + .from(userBook) + .join(readingRecord).on(readingRecord.userBookId.eq(userBook.id)) + .where( + userBook.userIdEq(userId), + userBook.isNotDeleted() + ) + .groupBy(userBook.id) + .orderBy(readingRecord.updatedAt.max().desc()) + .fetch() + } + + override fun findUnrecordedBooksSortedByPriority( + userId: UUID, + excludeIds: Set, + limit: Int + ): List { + return queryFactory + .selectFrom(userBook) + .where( + userBook.userIdEq(userId), + userBook.isNotDeleted(), + userBook.hasNoRecords(), + userBook.idNotIn(excludeIds) + ) + .orderBy( + statusPriorityOrder(), + userBook.updatedAt.desc() + ) + .limit(limit.toLong()) + .fetch() + } + + private fun QUserBookEntity.userIdEq(userId: UUID): BooleanExpression { + return this.userId.eq(userId) + } + + private fun QUserBookEntity.isNotDeleted(): BooleanExpression { + return this.deletedAt.isNull + } + + private fun QUserBookEntity.hasNoRecords(): BooleanExpression { + return JPAExpressions.selectOne() + .from(readingRecord) + .where(readingRecord.userBookId.eq(this.id)) + .notExists() + } + + private fun QUserBookEntity.idNotIn(excludeIds: Set): BooleanExpression? { + return if (excludeIds.isNotEmpty()) { + this.id.notIn(excludeIds) + } else { + null + } + } + + private fun statusPriorityOrder(): OrderSpecifier<*> { + return CaseBuilder() + .`when`(userBook.status.eq(BookStatus.READING)).then(1) + .`when`(userBook.status.eq(BookStatus.COMPLETED)).then(2) + .`when`(userBook.status.eq(BookStatus.BEFORE_READING)).then(3) + .otherwise(4) + .asc() + } + private fun statusEq(status: BookStatus?): BooleanExpression? { return status?.let { userBook.status.eq(it) } } 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 26778396..3cda1f8f 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 @@ -4,16 +4,17 @@ import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable 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.UserBookRepository import org.yapp.domain.userbook.UserBookSortType import org.yapp.infra.userbook.entity.UserBookEntity import org.yapp.infra.userbook.repository.JpaUserBookRepository +import java.time.LocalDateTime import java.util.* @Repository class UserBookRepositoryImpl( - private val jpaUserBookRepository: JpaUserBookRepository + private val jpaUserBookRepository: JpaUserBookRepository, ) : UserBookRepository { override fun findByUserIdAndBookIsbn(userId: UUID, isbn: String): UserBook? { @@ -56,11 +57,28 @@ class UserBookRepositoryImpl( title: String?, pageable: Pageable ): Page { - return jpaUserBookRepository.findUserBooksByDynamicCondition(userId, status, sort, title, pageable) - .map { it.toDomain() } + val page = jpaUserBookRepository.findUserBooksByDynamicCondition(userId, status, sort, title, pageable) + return page.map { it.toDomain() } } override fun countUserBooksByStatus(userId: UUID, status: BookStatus): Long { return jpaUserBookRepository.countUserBooksByStatus(userId, status) } + + override fun findRecordedBooksSortedByRecency(userId: UUID): List> { + val userBookLastRecordsProjections = jpaUserBookRepository.findRecordedBooksSortedByRecency(userId) + + return userBookLastRecordsProjections.map { projection -> + Triple(projection.userBookEntity.toDomain(), projection.lastRecordedAt, projection.recordCount) + } + } + + override fun findUnrecordedBooksSortedByPriority( + userId: UUID, + limit: Int, + excludeIds: Set + ): List { + val entities = jpaUserBookRepository.findUnrecordedBooksSortedByPriority(userId, excludeIds, limit) + return entities.map { it.toDomain() } + } }