Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -17,6 +17,7 @@ data class UserBookResponse private constructor(
val publisher: String,
val createdAt: String,
val updatedAt: String,
val recordCount: Int,
) {
companion object {
fun from(
Expand All @@ -33,6 +34,7 @@ data class UserBookResponse private constructor(
publisher = BookDataValidator.removeParenthesesFromPublisher(userBook.publisher),
createdAt = userBook.createdAt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
updatedAt = userBook.updatedAt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
recordCount = userBook.recordCount,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ data class ReadingRecordResponse private constructor(
val bookPublisher: String?,

@Schema(description = "도서 썸네일 URL", example = "https://example.com/book-cover.jpg")
val bookCoverImageUrl: String?
val bookCoverImageUrl: String?,

@Schema(description = "저자", example = "로버트 C. 마틴")
val author: String?
) {
companion object {
private val dateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME
Expand All @@ -56,7 +59,8 @@ data class ReadingRecordResponse private constructor(
updatedAt = readingRecordInfoVO.updatedAt.format(dateTimeFormatter),
bookTitle = readingRecordInfoVO.bookTitle,
bookPublisher = readingRecordInfoVO.bookPublisher,
bookCoverImageUrl = readingRecordInfoVO.bookCoverImageUrl
bookCoverImageUrl = readingRecordInfoVO.bookCoverImageUrl,
author = readingRecordInfoVO.author
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import org.yapp.domain.userbook.UserBookRepository
import org.yapp.globalutils.annotation.DomainService
import java.util.UUID

import org.yapp.domain.userbook.exception.UserBookNotFoundException
import org.yapp.domain.userbook.exception.UserBookErrorCode

@DomainService
class ReadingRecordDomainService(
private val readingRecordRepository: ReadingRecordRepository,
Expand All @@ -28,6 +31,11 @@ class ReadingRecordDomainService(
review: String,
emotionTags: List<String>
): ReadingRecordInfoVO {
val userBook = userBookRepository.findById(userBookId)
?: throw UserBookNotFoundException(
UserBookErrorCode.USER_BOOK_NOT_FOUND,
"User book not found with id: $userBookId"
)

val readingRecord = ReadingRecord.create(
userBookId = userBookId,
Expand All @@ -50,17 +58,19 @@ class ReadingRecordDomainService(
}
readingRecordTagRepository.saveAll(readingRecordTags)

val userBook = userBookRepository.findById(userBookId)
userBookRepository.save(userBook.increaseReadingRecordCount())

return ReadingRecordInfoVO.newInstance(
readingRecord = savedReadingRecord,
emotionTags = tags.map { it.name },
bookTitle = userBook?.title,
bookPublisher = userBook?.publisher,
bookCoverImageUrl = userBook?.coverImageUrl
bookTitle = userBook.title,
bookPublisher = userBook.publisher,
bookCoverImageUrl = userBook.coverImageUrl,
author = userBook.author
)
Comment on lines 63 to 70
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

성능 최적화 고려사항

createReadingRecord 메서드에서 이미 UserBook을 조회했으므로, ReadingRecordInfoVO.newInstance 호출 시 재조회하는 대신 이미 가져온 userBook 객체를 직접 사용하는 것이 더 효율적일 수 있습니다.

현재는 buildReadingRecordInfoVO 메서드 내부에서 UserBook을 다시 조회하고 있습니다. 다음과 같이 리팩터링을 고려해보세요:

return ReadingRecordInfoVO.newInstance(
    readingRecord = savedReadingRecord,
    emotionTags = tags.map { it.name },
-   bookTitle = userBook.title,
-   bookPublisher = userBook.publisher,
-   bookCoverImageUrl = userBook.coverImageUrl,
-   author = userBook.author
+   bookTitle = userBook.title,
+   bookPublisher = userBook.publisher,  
+   bookCoverImageUrl = userBook.coverImageUrl,
+   author = userBook.author
)

또는 buildReadingRecordInfoVO 메서드를 오버로드하여 이미 조회된 UserBook을 받도록 수정할 수 있습니다.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt
around lines 63 to 70, the buildReadingRecordInfoVO method currently re-fetches
the UserBook even though it was already retrieved in createReadingRecord. To
optimize performance, refactor buildReadingRecordInfoVO to accept the already
fetched userBook as a parameter or overload it to do so, and use this passed
userBook directly instead of querying again.

}


fun findReadingRecordById(readingRecordId: UUID): ReadingRecordInfoVO {
val readingRecord = readingRecordRepository.findById(readingRecordId)
?: throw ReadingRecordNotFoundException(
Expand All @@ -82,7 +92,8 @@ class ReadingRecordDomainService(
emotionTags = tags.map { it.name },
bookTitle = userBook?.title,
bookPublisher = userBook?.publisher,
bookCoverImageUrl = userBook?.coverImageUrl
bookCoverImageUrl = userBook?.coverImageUrl,
author = userBook?.author
)
}

Expand All @@ -105,7 +116,8 @@ class ReadingRecordDomainService(
emotionTags = tags.map { it.name },
bookTitle = userBook?.title,
bookPublisher = userBook?.publisher,
bookCoverImageUrl = userBook?.coverImageUrl
bookCoverImageUrl = userBook?.coverImageUrl,
author = userBook?.author
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ data class ReadingRecordInfoVO private constructor(
val updatedAt: LocalDateTime,
val bookTitle: String? = null,
val bookPublisher: String? = null,
val bookCoverImageUrl: String? = null
val bookCoverImageUrl: String? = null,
val author: String? = null
) {
init {
require(emotionTags.size <= 3) { "Maximum 3 emotion tags are allowed" }
Expand All @@ -29,7 +30,8 @@ data class ReadingRecordInfoVO private constructor(
emotionTags: List<String>,
bookTitle: String? = null,
bookPublisher: String? = null,
bookCoverImageUrl: String? = null
bookCoverImageUrl: String? = null,
author: String? = null
): ReadingRecordInfoVO {
return ReadingRecordInfoVO(
id = readingRecord.id,
Expand All @@ -42,7 +44,8 @@ data class ReadingRecordInfoVO private constructor(
updatedAt = readingRecord.updatedAt ?: throw IllegalStateException("updatedAt은 null일 수 없습니다."),
bookTitle = bookTitle,
bookPublisher = bookPublisher,
bookCoverImageUrl = bookCoverImageUrl
bookCoverImageUrl = bookCoverImageUrl,
author = author
)
}
}
Expand Down
8 changes: 8 additions & 0 deletions domain/src/main/kotlin/org/yapp/domain/userbook/UserBook.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ data class UserBook private constructor(
val title: String,
val author: String,
val status: BookStatus,
val readingRecordCount: Int = 0,
val createdAt: LocalDateTime? = null,
val updatedAt: LocalDateTime? = null,
val deletedAt: LocalDateTime? = null,
Expand All @@ -23,6 +24,11 @@ data class UserBook private constructor(
return this.copy(status = newStatus)
}

fun increaseReadingRecordCount(): UserBook {
return this.copy(readingRecordCount = this.readingRecordCount + 1)
}


companion object {
fun create(
userId: UUID,
Expand Down Expand Up @@ -57,6 +63,7 @@ data class UserBook private constructor(
title: String,
author: String,
status: BookStatus,
readingRecordCount: Int,
createdAt: LocalDateTime? = null,
updatedAt: LocalDateTime? = null,
deletedAt: LocalDateTime? = null
Expand All @@ -71,6 +78,7 @@ data class UserBook private constructor(
title = title,
author = author,
status = status,
readingRecordCount = readingRecordCount,
createdAt = createdAt,
updatedAt = updatedAt,
deletedAt = deletedAt
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class UserBookDomainService(
)

val savedUserBook = userBookRepository.save(userBook)
return UserBookInfoVO.newInstance(savedUserBook)
return UserBookInfoVO.newInstance(savedUserBook, savedUserBook.readingRecordCount)
}

fun findUserBooksByDynamicCondition(
Expand All @@ -46,20 +46,20 @@ class UserBookDomainService(
pageable: Pageable
): Page<UserBookInfoVO> {
val page = userBookRepository.findUserBooksByDynamicCondition(userId, status, sort, title, pageable)
return page.map { UserBookInfoVO.newInstance(it) }
return page.map { UserBookInfoVO.newInstance(it, it.readingRecordCount) }
}

fun findAllByUserIdAndBookIsbnIn(userId: UUID, isbns: List<String>): List<UserBookInfoVO> {
if (isbns.isEmpty()) {
return emptyList()
}
val userBooks = userBookRepository.findAllByUserIdAndBookIsbnIn(userId, isbns)
return userBooks.map { UserBookInfoVO.newInstance(it) }
return userBooks.map { UserBookInfoVO.newInstance(it, it.readingRecordCount) }
}

fun findByUserIdAndBookIsbn(userId: UUID, isbn: String): UserBookInfoVO? {
val userBook = userBookRepository.findByUserIdAndBookIsbn(userId, isbn)
return userBook?.let { UserBookInfoVO.newInstance(it) }
return userBook?.let { UserBookInfoVO.newInstance(it, it.readingRecordCount) }
}

fun getUserBookStatusCounts(userId: UUID): UserBookStatusCountsVO {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,6 @@ interface UserBookRepository {
limit: Int,
excludeIds: Set<UUID>
): List<UserBook>


Comment on lines +39 to +40
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

불필요한 빈 줄 제거

인터페이스 끝에 추가된 빈 줄들이 불필요합니다. 코드 일관성을 위해 제거하는 것을 권장합니다.

-    
-    
 }
📝 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
}
🤖 Prompt for AI Agents
In domain/src/main/kotlin/org/yapp/domain/userbook/UserBookRepository.kt at
lines 39 to 40, there are unnecessary blank lines at the end of the interface.
Remove these extra blank lines to maintain code consistency and cleanliness.

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ data class UserBookInfoVO private constructor(
val author: String,
val status: BookStatus,
val createdAt: LocalDateTime,
val updatedAt: LocalDateTime
val updatedAt: LocalDateTime,
val recordCount: Int
) {
init {
require(coverImageUrl.isNotBlank()) { "표지 이미지 URL은 비어 있을 수 없습니다." }
require(publisher.isNotBlank()) { "출판사는 비어 있을 수 없습니다." }
require(title.isNotBlank()) { "도서 제목은 비어 있을 수 없습니다." }
require(author.isNotBlank()) { "저자는 비어 있을 수 없습니다." }
require(recordCount >= 0) { "독서 기록 수는 0 이상이어야 합니다." }
require(!createdAt.isAfter(updatedAt)) {
"생성일(createdAt)은 수정일(updatedAt)보다 이후일 수 없습니다."
}
Expand All @@ -30,6 +32,7 @@ data class UserBookInfoVO private constructor(
companion object {
fun newInstance(
userBook: UserBook,
recordCount: Int
): UserBookInfoVO {
return UserBookInfoVO(
id = userBook.id,
Expand All @@ -42,7 +45,8 @@ data class UserBookInfoVO private constructor(
author = userBook.author,
status = userBook.status,
createdAt = userBook.createdAt ?: throw IllegalStateException("createdAt은 null일 수 없습니다."),
updatedAt = userBook.updatedAt ?: throw IllegalStateException("updatedAt은 null일 수 없습니다.")
updatedAt = userBook.updatedAt ?: throw IllegalStateException("updatedAt은 null일 수 없습니다."),
recordCount = recordCount
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ class UserBookEntity(
var status: BookStatus = status
protected set

@Column(name = "reading_record_count", nullable = false)
var readingRecordCount: Int = 0
protected set

fun toDomain(): UserBook = UserBook.reconstruct(
id = UserBook.Id.newInstance(this.id),
userId = UserBook.UserId.newInstance(this.userId),
Expand All @@ -76,6 +80,7 @@ class UserBookEntity(
title = this.title,
author = this.author,
status = this.status,
readingRecordCount = this.readingRecordCount,
createdAt = this.createdAt,
updatedAt = this.updatedAt,
deletedAt = this.deletedAt
Expand All @@ -93,7 +98,9 @@ class UserBookEntity(
title = userBook.title,
author = userBook.author,
status = userBook.status,
)
).apply {
this.readingRecordCount = userBook.readingRecordCount
}
Comment on lines +101 to +103
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

도메인에서 엔티티로의 변환 로직 검증 필요

readingRecordCount가 apply 블록에서 설정되는 방식은 올바르지만, 이 카운트의 일관성 유지 방법을 확인해야 합니다. PR 목표에 따르면 현재는 읽기 기록 추가 시에만 증가하며, 향후 비즈니스 로직 검토가 필요하다고 명시되어 있습니다.

다음 사항들을 확인해 주세요:

  1. 읽기 기록 삭제 시 카운트 감소 로직이 구현되어 있는지
  2. 기존 데이터의 카운트 초기화 방법
  3. 카운트 불일치 발생 시 복구 방안

🏁 Script executed:

#!/bin/bash
# 읽기 기록 관련 increment/decrement 로직 확인
ast-grep --pattern 'incrementReadingRecordCount($_)'
ast-grep --pattern 'decrementReadingRecordCount($_)'

# ReadingRecord 삭제 관련 로직 확인  
rg -A 5 "delete.*ReadingRecord"

Length of output: 143


🏁 Script executed:

#!/bin/bash
# infra/src 및 도메인 계층 전반에서 readingRecordCount 키워드 검색
rg -n "readingRecordCount"

# 증감 메서드 명 확인
rg -n "incrementReadingRecordCount"
rg -n "decrementReadingRecordCount"

# 읽기 기록 추가/삭제 메서드 확인
rg -n "addReadingRecord"
rg -n "removeReadingRecord"

# DB 삭제 쿼리나 리포지토리 삭제 메서드 검색
rg -n "delete.*ReadingRecord"

Length of output: 1675


ReadingRecordCount 동기화 로직 보완 필요

UserBookEntityreadingRecordCount를 도메인에서 엔티티로 복사하는 부분은 정상적이나, 검색 결과 아래 내용이 확인되지 않았습니다.

삭제 시 감소 구현 부재

  • addReadingRecord()로 증가만 처리되고, 삭제할 때 readingRecordCount를 감소시키는 메서드(removeReadingRecord() 또는 decrement…)가 코드베이스에 없습니다.
  • 비즈니스에서 읽기 기록 삭제를 허용한다면, 도메인·엔티티 양쪽에 감소 로직을 반드시 추가해야 합니다.

기존 데이터 초기화 방안 미확인

  • DB 마이그레이션 또는 초기화 스크립트에서 기존 레코드의 reading_record_count 컬럼을 백필(backfill)했는지 확인이 필요합니다.
  • 컬럼 추가 시 기본값이 0으로 설정되었다면 이미 생성된 레코드는 별도 처리 없이 0으로 초기화되었는지 검증해주세요.

불일치 복구 전략 부재

  • 운영 중 카운트가 도메인·엔티티 간 불일치가 발생할 경우를 대비해, 정합성 체크 및 복구(예: 배치 작업, 스크립트)를 설계하고 문서화해야 합니다.

위 항목들을 반영하여 다음을 진행해주세요.

  1. 삭제 시 readingRecordCount-- 로직 추가
  2. DB 마이그레이션·백필 스크립트 검토/작성
  3. 정합성 체크 및 복구 플로우 정의
🤖 Prompt for AI Agents
In infra/src/main/kotlin/org/yapp/infra/userbook/entity/UserBookEntity.kt around
lines 101 to 103, the readingRecordCount is only incremented but lacks a
corresponding decrement method for when reading records are deleted. Add a
method like removeReadingRecord() to decrement readingRecordCount in both the
domain and entity layers. Additionally, review or create DB migration and
backfill scripts to ensure existing records have correct initial
readingRecordCount values. Finally, define and document a consistency check and
recovery process to handle any discrepancies between domain and entity counts
during operation.

}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import java.util.*

@Repository
class UserBookRepositoryImpl(
private val jpaUserBookRepository: JpaUserBookRepository,
private val jpaUserBookRepository: JpaUserBookRepository
) : UserBookRepository {

override fun findByUserIdAndBookIsbn(userId: UUID, isbn: String): UserBook? {
Expand Down Expand Up @@ -81,4 +81,6 @@ class UserBookRepositoryImpl(
val entities = jpaUserBookRepository.findUnrecordedBooksSortedByPriority(userId, excludeIds, limit)
return entities.map { it.toDomain() }
}


Comment on lines +84 to +85
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

불필요한 빈 줄 제거

클래스 끝에 추가된 빈 줄들이 불필요합니다. 코드 일관성을 위해 제거하는 것을 권장합니다.

-

 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
infra/src/main/kotlin/org/yapp/infra/userbook/repository/impl/UserBookRepositoryImpl.kt
at lines 84 to 85, there are unnecessary blank lines at the end of the class.
Remove these extra blank lines to maintain code consistency and cleanliness.

}