Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1c89bdc
[BOOK-198] chore: domain - 공백 제거
move-hoon Jul 31, 2025
2da0ee9
[BOOK-198] chore: apis - 사용하지 않는 서비스 제거
move-hoon Jul 31, 2025
3c132b8
[BOOK-198] feat: apis - 홈 화면 API 구현
move-hoon Jul 31, 2025
f6ce3a5
[BOOK-198] feat: apis - 홈 화면 데이터 조회를 위한 HomeUseCase 클래스 추가
move-hoon Jul 31, 2025
8dbf3fe
[BOOK-198] feat: apis - 사용자 홈 화면 응답 DTO(UserHomeResponse) 추가
move-hoon Jul 31, 2025
c05c675
[BOOK-198] feat: domain, infra - 사용자 도서 목록과 마지막 기록 조회 기능 추가
move-hoon Jul 31, 2025
2ca765c
[BOOK-198] feat: domain - 사용자 도서와 마지막 기록 정보를 포함하는 UserBookWithLastRec…
move-hoon Jul 31, 2025
ce90a98
[BOOK-198] feat: domain - 사용자 도서 목록에서 마지막 기록과 함께 최근 읽은 도서 조회 기능 추가
move-hoon Jul 31, 2025
ec5cc12
[BOOK-198] feat: apis - 사용자 홈 화면에 최근 읽은 도서 조회 기능 추가
move-hoon Jul 31, 2025
cf98060
[BOOK-198] chore: apis - 사용하지 않는 import문 제거
move-hoon Jul 31, 2025
6b28d09
[BOOK-198] refactor: apis - limit 개수를 클라이언트에서 정해서 가져올 수 있도록 리팩토링
move-hoon Jul 31, 2025
3ebfba1
[BOOK-198] refactor: apis - limit 최소/최대 범위 검증 추가
move-hoon Jul 31, 2025
b05ead7
[BOOK-198] fix: apis - 카카오 로그인 관련 html 수정
move-hoon Jul 31, 2025
e3ae002
[BOOK-198] chore: domain - vo 네이밍 변경
move-hoon Jul 31, 2025
dc37da2
[BOOK-198] feat: domain, infra - 최근 읽은 도서 및 우선순위에 따라 정렬된 미기록 도서 조회 기능 추가
move-hoon Jul 31, 2025
6246d3a
[BOOK-198] feat: infra - 최근 기록에 따라 정렬된 도서 조회 및 미기록 도서 우선순위 조회 기능 추가
move-hoon Jul 31, 2025
f3763a6
[BOOK-198] feat: domain - 최근 기록에 따라 정렬된 도서 조회 및 미기록 도서 우선순위 조회 기능 개선
move-hoon Jul 31, 2025
ecb9f1a
[BOOK-198] refactor: apis - 사용자 홈 화면에 최근 읽은 도서 조회 로직 개선 및 미기록 도서 포함 기…
move-hoon Jul 31, 2025
e4dcdc4
[BOOK-198] docs: apis - 리드미 제거
move-hoon Jul 31, 2025
f9cc16b
[BOOK-198] refactor: apis - UserBookService에서 최근 읽은 도서 조회 로직 제거 및 Hom…
move-hoon Jul 31, 2025
33b763b
[BOOK-198] feat: apis - 파라미터 validated 로직 추가
move-hoon Jul 31, 2025
38841f5
[BOOK-198] refactor: domain - UserBook의 updatedAt이 null일 경우 예외 처리 추가
move-hoon Jul 31, 2025
7a01290
[BOOK-198] fix: apis - 컨트롤러 구현체의 limit 파라미터에 유효성 검사 추가
move-hoon Jul 31, 2025
0e2d446
Merge branch 'develop' into BOOK-198-feature/#64
move-hoon Jul 31, 2025
d07f92b
[BOOK-198] refactor: apis, domain, infra - 도서 기록 수 추가 및 관련 로직 수정
move-hoon Jul 31, 2025
274688d
Merge branch 'BOOK-198-feature/#64' of https://github.com/YAPP-Github…
move-hoon Jul 31, 2025
1bcff18
[BOOK-198] fix: infra - 누락된 매개변수 추가
move-hoon Jul 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -146,5 +141,4 @@ interface BookControllerApi {
@PageableDefault(size = 10, sort = ["createdAt"], direction = Sort.Direction.DESC)
pageable: Pageable
): ResponseEntity<UserBookPageResponse>

}
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

와일드카드 import 대신 구체적 import 권장

java.util.* 와일드카드 사용은 필요 이상의 타입 노출로 컴파일 성능과 가독성 모두에 불리합니다. UUID 하나만 사용하므로 구체적 import로 교체해주세요.

-import java.util.*
+import java.util.UUID
🤖 Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/book/service/UserBookService.kt at line
16, replace the wildcard import 'import java.util.*' with a specific import for
'java.util.UUID' since only UUID is used. This improves compile performance and
code clarity by limiting the imported types.


@Service
class UserBookService(
Expand Down Expand Up @@ -42,7 +41,6 @@ class UserBookService(
)
}


fun findAllByUserIdAndBookIsbnIn(userBooksByIsbnsRequest: UserBooksByIsbnsRequest): List<UserBookResponse> {
val userBooks = userBookDomainService.findAllByUserIdAndBookIsbnIn(
userBooksByIsbnsRequest.validUserId(),
Expand All @@ -51,6 +49,8 @@ class UserBookService(
return userBooks.map { UserBookResponse.from(it) }
}



private fun findUserBooksByDynamicCondition(
userId: UUID,
status: BookStatus?,
Expand Down
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.*
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

구체적인 클래스 임포트를 권장합니다.

-import java.util.*
+import java.util.UUID
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import java.util.*
-import java.util.*
+import java.util.UUID
🤖 Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/home/controller/HomeController.kt at line
14, replace the wildcard import 'import java.util.*' with explicit imports of
only the specific classes from java.util that are actually used in this file.
This improves code clarity and avoids unnecessary imports.


@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) }
)
}
}
}
36 changes: 36 additions & 0 deletions apis/src/main/kotlin/org/yapp/apis/home/service/HomeService.kt
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)
}

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
)

return booksWithRecords + booksWithoutRecords
}
}
18 changes: 18 additions & 0 deletions apis/src/main/kotlin/org/yapp/apis/home/usecase/HomeUseCase.kt
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
Expand Up @@ -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(
Expand All @@ -26,7 +24,6 @@ class ReadingRecordService(
): ReadingRecordResponse {
userBookService.validateUserBookExists(userId, userBookId)


val readingRecordInfoVO = readingRecordDomainService.createReadingRecord(
userBookId = userBookId,
pageNumber = request.validPageNumber(),
Expand Down
2 changes: 1 addition & 1 deletion apis/src/main/resources/static/kakao-login.html
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ <h1 style="text-align:center">소셜 로그인 테스트</h1>
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 => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ data class ReadingRecord private constructor(
val updatedAt: LocalDateTime? = null,
val deletedAt: LocalDateTime? = null,
) {


companion object {
fun create(
userBookId: UUID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -70,4 +71,36 @@ class UserBookDomainService(
fun findByIdAndUserId(userBookId: UUID, userId: UUID): UserBook? {
return userBookRepository.findByIdAndUserId(userBookId, userId)
}

fun findBooksWithRecordsOrderByLatest(userId: UUID): List<HomeBookVO> {
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<UUID>
): List<HomeBookVO> {
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
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

구체적인 클래스 임포트를 권장합니다.

import java.util.* 대신 필요한 클래스만 명시적으로 임포트하는 것이 좋습니다.

-import java.util.*
+import java.util.UUID
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import java.util.*
-import java.util.*
+import java.util.UUID
🤖 Prompt for AI Agents
In domain/src/main/kotlin/org/yapp/domain/userbook/UserBookRepository.kt at line
6, replace the wildcard import 'import java.util.*' with explicit imports of
only the specific classes from java.util that are actually used in the file.
This improves code clarity and avoids unnecessary imports.



interface UserBookRepository {
Expand All @@ -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>
}
Loading