-
Notifications
You must be signed in to change notification settings - Fork 1
feat: 홈 화면 도서 선별 로직 구현 #66
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1c89bdc
2da0ee9
3c132b8
f6ce3a5
8dbf3fe
c05c675
2ca765c
ce90a98
ec5cc12
cf98060
6b28d09
3ebfba1
b05ead7
e3ae002
dc37da2
6246d3a
f3763a6
ecb9f1a
e4dcdc4
f9cc16b
33b763b
38841f5
7a01290
0e2d446
d07f92b
274688d
1bcff18
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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.* | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) 와일드카드 import 대신 구체적 import 권장
-import java.util.*
+import java.util.UUID🤖 Prompt for AI Agents |
||
|
|
||
| @Service | ||
| class UserBookService( | ||
|
|
@@ -42,7 +41,6 @@ class UserBookService( | |
| ) | ||
| } | ||
|
|
||
|
|
||
| fun findAllByUserIdAndBookIsbnIn(userBooksByIsbnsRequest: UserBooksByIsbnsRequest): List<UserBookResponse> { | ||
| 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?, | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -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.* | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) 구체적인 클래스 임포트를 권장합니다. -import java.util.*
+import java.util.UUID📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||
|
|
||||||||
| @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<UserHomeResponse> { | ||||||||
| val homeData = homeUseCase.getUserHomeData(userId, limit) | ||||||||
| return ResponseEntity.ok(homeData) | ||||||||
| } | ||||||||
| } | ||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<UserHomeResponse> | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<RecentBookResponse> | ||
| ) { | ||
| 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<HomeBookVO>): UserHomeResponse { | ||
| return UserHomeResponse( | ||
| recentBooks = recentBooks.map { RecentBookResponse.from(it) } | ||
| ) | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
| } | ||
move-hoon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| private fun selectBooksForHome(userId: UUID, limit: Int): List<HomeBookVO> { | ||
| 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 | ||
| ) | ||
move-hoon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| return booksWithRecords + booksWithoutRecords | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -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.* | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) 구체적인 클래스 임포트를 권장합니다.
-import java.util.*
+import java.util.UUID📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||
|
|
||||||||
|
|
||||||||
| interface UserBookRepository { | ||||||||
|
|
@@ -28,4 +29,11 @@ interface UserBookRepository { | |||||||
|
|
||||||||
| fun countUserBooksByStatus(userId: UUID, status: BookStatus): Long | ||||||||
|
|
||||||||
| fun findRecordedBooksSortedByRecency(userId: UUID): List<Triple<UserBook, LocalDateTime, Long>> | ||||||||
|
|
||||||||
| fun findUnrecordedBooksSortedByPriority( | ||||||||
| userId: UUID, | ||||||||
| limit: Int, | ||||||||
| excludeIds: Set<UUID> | ||||||||
| ): List<UserBook> | ||||||||
| } | ||||||||
Uh oh!
There was an error while loading. Please reload this page.