Skip to content

Conversation

@minwoo1999
Copy link
Member

@minwoo1999 minwoo1999 commented Jul 24, 2025

🔗 관련 이슈


📘 작업 유형

  • Feature (기능 추가)

  • 🐞 Bugfix (버그 수정)

  • 🔧 Refactor (코드 리팩토링)

  • ⚙️ Chore (환경 설정)

  • 📝 Docs (문서 작성 및 수정)

  • ✅ Test (기능 테스트)

  • 🎨 style (코드 스타일 수정)


📙 작업 내역

📖 독서 기록(Reading Record) 기능 구현

  • 도메인 계층:

    • ReadingRecord 도메인 모델과 Id, UserBookId, PageNumber, Quote, Review, EmotionTag 등 관련 **Value Objects(VO)**를 정의했습니다.

    • ReadingRecordDomainService: 독서 기록 생성 및 동적 조건 조회를 위한 도메인 서비스를 구현했습니다.

    • ReadingRecordRepository: 독서 기록 데이터의 영속성(Persistence)을 담당하는 인터페이스를 정의했습니다.

    • ReadingRecordInfoVO: 독서 기록 정보를 나타내는 불변 VO를 추가했습니다.

  • API 계층:

    • ReadingRecordControllerApiReadingRecordController: 독서 기록 생성페이징 처리된 목록 조회를 위한 REST API 엔드포인트를 구현했습니다.

    • CreateReadingRecordRequest DTO: 독서 기록 생성을 위한 요청 객체로, 유효성 검증(@Min, @Max, @NotBlank, @Size)을 포함합니다.

    • ReadingRecordResponse DTO: 독서 기록 조회 결과를 반환하는 응답 객체를 정의했습니다.

  • 애플리케이션 서비스 계층:

    • ReadingRecordService: 독서 기록 생성 및 동적 조건 조회를 위한 애플리케이션 서비스를 구현했습니다.

    • ReadingRecordUseCase: UserAuthServiceUserBookService를 활용하여 사용자 및 사용자 책의 유효성을 검증하고, 독서 기록의 생성 및 조회 흐름을 조정하는 유스케이스를 구현했습니다.

📚 사용자 책(User Book) 관련 개선

  • UserBookService 리팩토링:

    • upsertUserBook 메서드의 파라미터 전달 방식을 변경하고 반환 값을 UserBookInfoVO로 통일했습니다.

    • 특정 userBookIduserId에 해당하는 UserBook의 존재 여부를 검증하고 반환하는 validateUserBookExists 메서드를 추가했습니다.

    • findAllUserBooks(userId: UUID) 메서드는 제거하고 validateUserBookExists로 대체했습니다.

  • 예외 처리 강화:

    • UserBookErrorCodeUserBookNotFoundException을 추가하여 사용자 책을 찾을 수 없을 때의 명확한 예외 처리를 구현했습니다.

  • BookSearchRequest 변경: companion object 내부에 of 팩토리 메서드를 추가하여 객체 생성을 위한 명시적인 방법을 제공합니다.

  • DTO 생성자 접근 제어자 변경: DeleteTokenRequest, FindUserIdentityRequest, TokenGenerateRequest, UserBooksByIsbnsRequest의 주 생성자를 private constructor로 변경하여 객체 생성 방식의 통일성을 높였습니다.

💾 인프라 계층

  • ReadingRecordEntity: 독서 기록 도메인 모델의 영속성 엔티티를 정의하고, BaseTimeEntity를 상속받아 생성 및 수정 시간을 자동으로 관리하도록 했습니다. @SQLDelete를 사용하여 소프트 딜리트(Soft Delete)를 구현했습니다.

  • Repository 구현: JpaReadingRecordRepository, JpaReadingRecordQuerydslRepository, ReadingRecordRepositoryImpl을 통해 독서 기록의 CRUD 및 동적 쿼리 기능을 지원합니다.

  • UserBookRepository 변경: findByIdAndUserId 메서드를 추가하여 사용자 ID 기반의 사용자 책 조회를 지원합니다.


🧪 테스트 내역

  • 브라우저/기기에서 동작 확인

  • 엣지 케이스 테스트 완료

  • 기존 기능 영향 없음


🎨 스크린샷 또는 시연 영상 (선택)

기능 | 미리보기 | 기능 | 미리보기 -- | -- | -- | -- 기능 설명 |   | 기능 설명 |  
Sheets로 내보내기

✅ PR 체크리스트

  • 커밋 메시지가 명확합니다

  • PR 제목이 컨벤션에 맞습니다

  • 관련 이슈 번호를 작성했습니다

  • 기능이 정상적으로 작동합니다

  • 불필요한 코드를 제거했습니다


💬 추가 설명 or 리뷰 포인트 (선택)

  • 독서 기록 생성 및 조회 로직: 사용자 및 사용자 책의 존재 여부를 검증하는 로직과 페이지네이션 및 정렬을 포함한 조회 로직의 적절성을 확인해주세요.

  • 새로운 예외 처리: UserBookNotFoundException 도입이 전반적인 예외 처리 전략과 잘 부합하는지 검토해주세요.

Summary by CodeRabbit

  • 신규 기능

    • 독서 기록(Reading Record) 관리 기능이 추가되었습니다. 사용자는 특정 책에 대한 독서 기록을 생성하고, 페이지별로 인용구, 리뷰, 감정 태그를 남길 수 있습니다.
    • 독서 기록 목록 조회 시 정렬 및 페이징 기능이 지원됩니다.
    • 독서 기록 관련 API가 Swagger 문서와 함께 제공됩니다.
    • 책, 사용자 책, 독서 기록, 태그 도메인 모델과 저장소가 확장되어 UUID 기반 식별자를 도입하였습니다.
  • 기능 개선

    • 사용자 책 및 독서 기록 목록 조회 시 정렬 옵션이 문자열에서 Enum 타입으로 변경되어 명확한 정렬 기준 선택이 가능합니다.
    • 사용자 책 관련 오류 코드와 예외가 세분화되어, 오류 발생 시 더 명확한 안내가 제공됩니다.
    • 사용자 책, 토큰, 사용자 식별 요청 객체의 생성 방식이 개선되어 안전한 인스턴스 생성이 보장됩니다.
  • 버그 수정

    • 기존 사용자 책 조회 메서드가 제거되고, 사용자 책 존재 검증 기능이 추가되었습니다.
  • 문서화

    • 독서 기록 관련 API에 Swagger 문서가 추가되어, 사용 방법과 응답 구조를 쉽게 확인할 수 있습니다.

@coderabbitai
Copy link

coderabbitai bot commented Jul 24, 2025

📝 Walkthrough

"""

Walkthrough

이번 변경 사항은 독서기록(ReadingRecord) 도메인에 대한 전체적인 API, 서비스, 도메인, 인프라 계층의 신규 도입과 함께, UserBook 관련 정렬 방식의 enum 도입 및 적용, UserBook 조회 및 검증 로직 강화, 예외 및 에러코드 신설, 일부 DTO 생성자 접근 제어 강화 등이 포함됩니다. 또한, 도메인-인프라 계층 간 매핑 및 쿼리 메서드가 대거 추가되었습니다.

Changes

파일/그룹 변경 요약
apis/…/auth/dto/request/*.kt 여러 DTO의 생성자(private) 접근제어로 변경 및 팩토리 메서드 사용 강제
apis/…/book/controller/BookController.kt
apis/…/book/controller/BookControllerApi.kt
apis/…/book/usecase/BookUseCase.kt
UserBook 정렬 파라미터를 String? → UserBookSortType? enum으로 변경
apis/…/book/dto/response/BookDetailResponse.kt 생성자 trailing comma 추가(무관)
apis/…/book/exception/UserBookErrorCode.kt
apis/…/book/exception/UserBookNotFoundException.kt
UserBook 에러코드 enum 및 NotFoundException 신설
apis/…/book/service/UserBookService.kt findAllUserBooks 제거, validateUserBookExists 도입, 동적조회 정렬타입 enum 적용 등
apis/…/readingrecord/controller/*.kt
apis/…/readingrecord/dto/request/CreateReadingRecordRequest.kt
apis/…/readingrecord/dto/response/ReadingRecordResponse.kt
apis/…/readingrecord/service/ReadingRecordService.kt
apis/…/readingrecord/usecase/ReadingRecordUseCase.kt
독서기록 API 컨트롤러, 서비스, 유스케이스, DTO 등 신규 도입 및 전체 흐름 구현
domain/…/readingrecord/*.kt ReadingRecord 도메인, 도메인서비스, 리포지토리, 정렬타입 enum, VO 등 신규 도입
domain/…/userbook/UserBookDomainService.kt
domain/…/userbook/UserBookRepository.kt
domain/…/userbook/vo/UserBookInfoVO.kt
UserBook upsert 시 bookId 파라미터 추가, findByIdAndUserId 등 검증 메서드 추가, 정렬타입 enum 적용, VO에 bookId 필드 추가
domain/…/userbook/UserBookSortType.kt UserBook 정렬 enum 신설
infra/…/readingrecord/entity/ReadingRecordEntity.kt
infra/…/readingrecord/repository/*.kt
ReadingRecord JPA 엔티티, Querydsl/Repository/Impl 등 인프라 계층 신규 도입 및 매핑 구현
infra/…/userbook/repository/*.kt UserBook findByIdAndUserId 등 신규 쿼리 메서드, 정렬타입 enum 적용
domain/…/book/Book.kt
domain/…/book/BookDomainService.kt
domain/…/book/BookRepository.kt
domain/…/book/vo/BookInfoVO.kt
infra/…/book/entity/BookEntity.kt
infra/…/book/repository/JpaBookRepository.kt
infra/…/book/repository/impl/BookRepositoryImpl.kt
Book 도메인 ID 타입 UUID 전환, ISBN 별도 필드 유지, 관련 리포지토리 및 엔티티 수정
apis/…/book/dto/request/UpsertUserBookRequest.kt
apis/…/book/dto/response/BookCreateResponse.kt
apis/…/book/dto/response/UserBookPageResponse.kt
UpsertUserBookRequest에 bookId 필드 추가 및 관련 팩토리 메서드 수정, BookCreateResponse에 bookId 추가, UserBookPageResponse에 totalCount 필드 추가
domain/…/readingrecordtag/.kt
domain/…/tag/
.kt
infra/…/readingrecordtag/entity/.kt
infra/…/readingrecordtag/repository/
.kt
infra/…/tag/entity/.kt
infra/…/tag/repository/
.kt
ReadingRecordTag, Tag 도메인 및 인프라 계층 신규 도입 및 매핑 구현
domain/…/readingrecordtag/ReadingRecordTagRepository.kt
domain/…/tag/TagRepository.kt
infra/…/readingrecordtag/repository/ReadingRecordTagRepositoryImpl.kt
infra/…/tag/repository/TagRepositoryImpl.kt
ReadingRecordTag, Tag 리포지토리 인터페이스 및 구현체 신규 도입
infra/…/userbook/entity/UserBookEntity.kt UserBookEntity에 bookId 필드 추가 및 soft delete 필터링 적용
infra/…/userbook/repository/JpaUserBookRepository.kt
infra/…/userbook/repository/impl/UserBookRepositoryImpl.kt
infra/…/userbook/repository/JpaUserBookQuerydslRepository.kt
infra/…/userbook/repository/impl/JpaUserBookQuerydslRepositoryImpl.kt
UserBook 정렬 enum 적용, findByBookIdAndUserId 및 findByIdAndUserId 신규 쿼리 메서드 추가
infra/…/external/aladin/response/AladinResponseBase.kt BookItem 데이터 클래스에 itemPage 필드 추가 및 publisher nullability 변경

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant ReadingRecordController
    participant ReadingRecordUseCase
    participant UserAuthService
    participant UserBookService
    participant ReadingRecordService
    participant ReadingRecordDomainService
    participant ReadingRecordRepository

    Client->>ReadingRecordController: POST /api/v1/reading-records/{userBookId}
    ReadingRecordController->>ReadingRecordUseCase: createReadingRecord(userId, userBookId, request)
    ReadingRecordUseCase->>UserAuthService: validateUser(userId)
    ReadingRecordUseCase->>UserBookService: validateUserBookExists(userId, userBookId)
    ReadingRecordUseCase->>ReadingRecordService: createReadingRecord(userId, userBookId, request)
    ReadingRecordService->>ReadingRecordDomainService: createReadingRecord(userBookId, pageNumber, quote, review, emotionTags)
    ReadingRecordDomainService->>ReadingRecordRepository: save(ReadingRecord)
    ReadingRecordRepository-->>ReadingRecordDomainService: ReadingRecord
    ReadingRecordDomainService-->>ReadingRecordService: ReadingRecordInfoVO
    ReadingRecordService-->>ReadingRecordUseCase: ReadingRecordResponse
    ReadingRecordUseCase-->>ReadingRecordController: ReadingRecordResponse
    ReadingRecordController-->>Client: 201 Created + ReadingRecordResponse
Loading
sequenceDiagram
    participant Client
    participant ReadingRecordController
    participant ReadingRecordUseCase
    participant UserAuthService
    participant UserBookService
    participant ReadingRecordService
    participant ReadingRecordDomainService
    participant ReadingRecordRepository

    Client->>ReadingRecordController: GET /api/v1/reading-records/{userBookId}
    ReadingRecordController->>ReadingRecordUseCase: getReadingRecordsByUserBookId(userId, userBookId, sort, pageable)
    ReadingRecordUseCase->>UserAuthService: validateUser(userId)
    ReadingRecordUseCase->>UserBookService: validateUserBookExists(userId, userBookId)
    ReadingRecordUseCase->>ReadingRecordService: getReadingRecordsByDynamicCondition(userBookId, sort, pageable)
    ReadingRecordService->>ReadingRecordDomainService: findReadingRecordsByDynamicCondition(userBookId, sort, pageable)
    ReadingRecordDomainService->>ReadingRecordRepository: findReadingRecordsByDynamicCondition(userBookId, sort, pageable)
    ReadingRecordRepository-->>ReadingRecordDomainService: Page<ReadingRecord>
    ReadingRecordDomainService-->>ReadingRecordService: Page<ReadingRecordInfoVO>
    ReadingRecordService-->>ReadingRecordUseCase: Page<ReadingRecordResponse>
    ReadingRecordUseCase-->>ReadingRecordController: Page<ReadingRecordResponse>
    ReadingRecordController-->>Client: 200 OK + Page<ReadingRecordResponse>
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40–60 minutes

  • 신규 도메인/인프라/서비스/컨트롤러/DTO/유스케이스 등 다수 파일 추가 및 다계층 연동
  • 기존 UserBook 관련 API/서비스/도메인/리포지토리/VO의 정렬 방식 enum화 및 검증 로직 추가
  • 예외 및 에러코드 신설, 접근제어 변경 등 설계적 영향도 존재

Possibly related PRs

  • YAPP-Github/Reed-Server#37: UpsertUserBookRequest와 관련된 DTO 및 도메인/서비스 계층 변경 사항과 직접적인 연관
  • YAPP-Github/Reed-Server#49: UserBook 동적 조회 기능과 관련된 도메인, 리포지토리, API 변경 사항과 강한 코드 레벨 연관
  • YAPP-Github/Reed-Server#31: UserBook 도메인 및 서비스 계층 구조, API 레이어, UserBook 등록/조회 등과 직접적으로 코드 및 아키텍처 레벨에서 연관된 변경

Suggested reviewers

  • hoonyworld
    """

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch BOOK-163-feature/#52

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions github-actions bot requested a review from move-hoon July 24, 2025 15:09
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🔭 Outside diff range comments (2)
apis/src/main/kotlin/org/yapp/apis/auth/dto/request/TokenGenerateRequest.kt (1)

30-39: 팩토리 메서드 검증 로직 강화 제안
TokenGenerateRequest.of 에서 최소한의 검증만 수행되고 있으며, 코드베이스 내 다른 토큰 DTO는 TokenGenerateRequest 하나뿐이라 일관성 이슈는 없습니다. 다음 사항을 추가로 고려해주세요:

  • JWT 형식 검증
    • 정규표현식(JWT_REGEX) 또는 JwtDecoder/Jwts.parserBuilder() 등을 이용해 header.payload.signature 구조를 검증
  • 만료 시간 상한선 검증
    • 예: require(expiration in 1..MAX_EXPIRATION_MS) { "Expiration은 1 이상 ${MAX_EXPIRATION_MS} 이하여야 합니다." }
  • 검증 책임 위치 검토
    • DTO 내부가 아닌 서비스 레이어나 전용 JWT 파서 라이브러리에서 처리하는 방안도 함께 검토

수정 위치

  • apis/src/main/kotlin/org/yapp/apis/auth/dto/request/TokenGenerateRequest.kt – companion object’s of(...) 메서드
apis/src/main/kotlin/org/yapp/apis/auth/dto/request/UserBooksByIsbnsRequest.kt (1)

34-36: 팩토리 메서드에 추가 검증 로직 고려해보세요.

TokenGenerateRequest의 팩토리 메서드와 달리 이 메서드는 추가적인 유효성 검증을 수행하지 않습니다. ISBN 목록의 유효성(예: 빈 문자열, 유효한 ISBN 형식) 검증을 추가하는 것을 고려해보세요.

 companion object {
     fun of(userId: UUID, isbns: List<String>): UserBooksByIsbnsRequest {
+        require(isbns.isNotEmpty()) { "ISBN 목록이 비어있을 수 없습니다." }
+        require(isbns.all { it.isNotBlank() }) { "ISBN은 공백일 수 없습니다." }
         return UserBooksByIsbnsRequest(userId = userId, isbns = isbns)
     }
 }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between beff1d0 and 78e9e6c.

📒 Files selected for processing (27)
  • apis/src/main/kotlin/org/yapp/apis/auth/dto/request/DeleteTokenRequest.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/dto/request/FindUserIdentityRequest.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/dto/request/TokenGenerateRequest.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/dto/request/UserBooksByIsbnsRequest.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookSearchRequest.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/book/exception/UserBookErrorCode.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/book/exception/UserBookNotFoundException.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/book/service/UserBookService.kt (2 hunks)
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordController.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerApi.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequest.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/ReadingRecordResponse.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordService.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCase.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecord.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordRepository.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/readingrecord/vo/ReadingRecordInfoVO.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/userbook/UserBookDomainService.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/userbook/UserBookRepository.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/infra/readingrecord/entity/ReadingRecordEntity.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/JpaReadingRecordQuerydslRepository.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/JpaReadingRecordRepository.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/JpaReadingRecordQuerydslRepositoryImpl.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/ReadingRecordRepositoryImpl.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/infra/userbook/repository/JpaUserBookRepository.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/infra/userbook/repository/impl/UserBookRepositoryImpl.kt (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
domain/src/main/kotlin/org/yapp/domain/readingrecord/vo/ReadingRecordInfoVO.kt (1)

Learnt from: hoonyworld
PR: #39
File: apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookSearchResponse.kt:45-45
Timestamp: 2025-07-17T15:09:36.267Z
Learning: Response DTO 클래스에서는 불변성을 지키기 위해 프로퍼티를 val로 선언하고, 업데이트가 필요한 경우 copy() 메서드를 사용해야 합니다.

apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/ReadingRecordResponse.kt (1)

Learnt from: hoonyworld
PR: #39
File: apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookSearchResponse.kt:45-45
Timestamp: 2025-07-17T15:09:36.267Z
Learning: Response DTO 클래스에서는 불변성을 지키기 위해 프로퍼티를 val로 선언하고, 업데이트가 필요한 경우 copy() 메서드를 사용해야 합니다.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-validation
🔇 Additional comments (51)
apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookSearchRequest.kt (1)

6-6: 생성자를 private으로 변경한 것은 좋은 접근입니다.

캡슐화를 강화하고 객체 생성을 제어하기 위해 생성자를 private으로 만든 것은 좋은 설계 결정입니다. 이는 다른 request DTO들과 일관성을 유지하는 패턴이기도 합니다.

apis/src/main/kotlin/org/yapp/apis/auth/dto/request/DeleteTokenRequest.kt (1)

11-23: 생성자 접근 제어 패턴 적용이 적절합니다.

private 생성자와 companion object의 팩토리 메서드를 통한 객체 생성 패턴이 올바르게 적용되었습니다. 이는 객체 생성을 제어하고 일관된 인스턴스화 방식을 제공하는 좋은 설계입니다.

domain/src/main/kotlin/org/yapp/domain/userbook/UserBookRepository.kt (1)

12-12: 새로운 조회 메서드가 적절하게 추가되었습니다.

findByIdAndUserId 메서드는 사용자 ID와 도서 ID를 모두 검증해야 하는 요구사항을 충족하며, 기존 메서드들과 일관된 네이밍 패턴을 따르고 있습니다. nullable 반환 타입도 적절합니다.

infra/src/main/kotlin/org/yapp/infra/userbook/repository/JpaUserBookRepository.kt (1)

9-9: JPA 쿼리 메서드가 올바르게 추가되었습니다.

Spring Data JPA의 메서드 네이밍 규칙에 따라 findByIdAndUserId 메서드가 적절하게 정의되었으며, 도메인 레이어의 요구사항과 일치합니다.

apis/src/main/kotlin/org/yapp/apis/book/exception/UserBookNotFoundException.kt (1)

5-8: 예외 클래스가 적절하게 구현되었습니다.

CommonException을 상속받아 일관된 예외 처리 패턴을 따르고 있으며, UserBookErrorCode와 선택적 메시지를 받는 구조가 적절합니다. 단일 책임 원칙을 잘 준수한 간결한 구현입니다.

apis/src/main/kotlin/org/yapp/apis/auth/dto/request/FindUserIdentityRequest.kt (1)

12-27: 일관된 생성자 접근 제어 패턴이 적용되었습니다.

다른 인증 관련 DTO들과 동일한 private 생성자와 팩토리 메서드 패턴을 적용하여 코드베이스 전반의 일관성을 유지하고 있습니다. UserIdResponse로부터의 변환 로직도 명확합니다.

apis/src/main/kotlin/org/yapp/apis/auth/dto/request/TokenGenerateRequest.kt (1)

12-12: 생성자 접근 제어 개선이 잘 적용되었습니다.

private 생성자와 companion object의 팩토리 메서드를 통한 객체 생성 패턴이 캡슐화를 강화하고 유효성 검증을 보장합니다.

apis/src/main/kotlin/org/yapp/apis/auth/dto/request/UserBooksByIsbnsRequest.kt (1)

12-12: 생성자 접근 제어 패턴이 일관되게 적용되었습니다.

다른 요청 DTO들과 동일한 private 생성자 패턴을 사용하여 객체 생성을 제어하고 있습니다.

domain/src/main/kotlin/org/yapp/domain/userbook/UserBookDomainService.kt (1)

67-69: 새로운 사용자 도서 조회 메서드가 올바르게 구현되었습니다.

findByIdAndUserId 메서드는 사용자 ID와 도서 ID를 모두 검증하여 보안을 강화하는 좋은 접근 방식입니다. 단순한 리포지토리 위임 구조로 도메인 서비스의 역할에 적합합니다.

infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/JpaReadingRecordQuerydslRepository.kt (1)

8-15: QueryDSL 기반 동적 쿼리 인터페이스가 잘 설계되었습니다.

메서드 시그니처가 명확하고 Spring Data의 페이징 규칙을 잘 따르고 있습니다. userBookId를 통한 필터링과 선택적 정렬 파라미터를 제공하는 구조가 적절합니다.

infra/src/main/kotlin/org/yapp/infra/userbook/repository/impl/UserBookRepositoryImpl.kt (2)

20-20: 구문 오류 수정이 적절히 처리되었습니다.

빠진 중괄호가 추가되어 메서드가 올바르게 완성되었습니다.


22-24: 새로운 조회 메서드가 일관된 패턴으로 구현되었습니다.

findByIdAndUserId 메서드가 기존 리포지토리 메서드들과 동일한 패턴(JPA 리포지토리 위임 → 도메인 객체 변환)을 따르고 있어 코드의 일관성이 유지됩니다.

apis/src/main/kotlin/org/yapp/apis/book/exception/UserBookErrorCode.kt (1)

6-16: 오류 코드 정의가 잘 구현되었습니다.

BaseErrorCode 인터페이스를 올바르게 구현하고 있으며, HTTP 상태, 오류 코드, 메시지가 적절히 정의되어 있습니다. 한국어 메시지도 명확하고 사용자 친화적입니다.

apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/ReadingRecordResponse.kt (2)

10-34: 응답 DTO 구조가 잘 설계되었습니다.

불변성을 지키기 위해 모든 프로퍼티를 val로 선언했고, private 생성자와 팩토리 메서드 패턴을 사용하여 객체 생성을 제어하고 있습니다. Swagger 어노테이션도 적절히 작성되어 API 문서화에 도움이 됩니다.


38-49: 도메인 VO에서 응답 DTO로의 변환 로직이 적절합니다.

ReadingRecordInfoVO의 모든 필드를 올바르게 매핑하고 있으며, 중첩된 값 객체들의 실제 값을 추출하는 방식과 날짜 포맷팅이 적절합니다.

domain/src/main/kotlin/org/yapp/domain/readingrecord/vo/ReadingRecordInfoVO.kt (2)

6-21: 도메인 값 객체의 구조와 검증 로직이 잘 구현되었습니다.

private 생성자와 팩토리 메서드 패턴을 사용하여 객체 생성을 제어하고, 도메인 규칙을 적절히 검증하고 있습니다. 감정 태그 개수 제한과 타임스탬프 일관성 검증이 포함되어 있습니다.


24-37: 팩토리 메서드가 적절히 구현되었습니다.

ReadingRecord 도메인 엔티티에서 값 객체로의 변환이 올바르게 이루어지고 있으며, null 타임스탬프에 대한 예외 처리도 적절합니다.

infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/JpaReadingRecordQuerydslRepositoryImpl.kt (2)

21-47: 동적 조건 조회 메서드가 적절히 구현되었습니다.

QueryDSL을 사용한 페이지네이션과 정렬 처리가 올바르게 구현되어 있습니다. 데이터 조회와 카운트 조회를 분리하여 정확한 페이지네이션 정보를 제공하고 있습니다.


49-57: 정렬 조건 처리가 명확하고 적절합니다.

다양한 정렬 옵션을 지원하고 기본값으로 생성일 내림차순을 사용하는 것이 합리적입니다. when 표현식을 사용하여 가독성도 좋습니다.

infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/JpaReadingRecordRepository.kt (1)

10-19: JPA 레포지토리 인터페이스가 잘 설계되었습니다.

JpaRepository와 커스텀 QueryDSL 레포지토리를 적절히 결합하고 있으며, Spring Data JPA 네이밍 컨벤션을 따르는 쿼리 메서드들이 다양한 사용 사례를 지원합니다. 페이지네이션 지원과 카운트 기능도 포함되어 있습니다.

apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequest.kt (2)

10-30: 잘 설계된 요청 DTO 클래스

private 생성자와 companion object factory 메서드 패턴을 사용하여 객체 생성을 제어하고, 검증 어노테이션들이 적절히 적용되어 있습니다. 에러 메시지도 일관성 있게 한국어로 작성되어 있고, 검증 규칙들도 합리적입니다.


27-29: Validation 의존성 및 중첩된 @SiZe 어노테이션 동작 여부 확인 필요

CreateReadingRecordRequest.kt의 emotionTags 필드에 적용된

val emotionTags: List<@Size(max = 10, message = "...") String>? = null

구조가 실제 Bean Validation(Hibernate Validator) 환경에서 의도한 대로 각 요소에 대해 검증이 수행되는지 확인이 필요합니다.

확인 사항:

  • 빌드 파일(build.gradle/build.gradle.kts/pom.xml)에 spring-boot-starter-validation, hibernate-validator, validation-api 등의 Bean Validation 관련 의존성이 포함되어 있는지 수동으로 검토해주세요.
  • Nested Bean Validation이 정상 동작하는지 간단한 유닛 테스트(예: Validator#validate() 호출)를 통해 각 리스트 요소가 최대 10자 제한을 넘어설 때 검증 오류가 발생하는지 검증해 보시기 바랍니다.
domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt (2)

9-12: 적절한 도메인 서비스 구조

@DomainService 어노테이션을 사용하고 Repository 의존성을 주입받는 구조가 잘 설계되어 있습니다. 도메인 로직과 영속성 로직이 적절히 분리되어 있습니다.


22-32: 도메인 객체 생성 및 VO 변환 로직 검증 완료

  • ReadingRecord.create()에서
    • id(UUID)
    • userBookId, pageNumber, quote, review, emotionTags
    가 모두 올바르게 초기화됩니다.
  • 저장된 엔티티는 JPA Auditing(@CreatedDate, @LastModifiedDate)에 따라 createdAt/updatedAt이 채워지고,
  • ReadingRecordInfoVO.newInstance()에서 id, userBookId, pageNumber, quote, review, emotionTags, createdAt, updatedAt 모두 VO에 정확히 매핑됩니다.

추가 수정 사항은 없습니다.

infra/src/main/kotlin/org/yapp/infra/readingrecord/entity/ReadingRecordEntity.kt (3)

13-13: 적절한 소프트 삭제 구현

@SQLDelete 어노테이션을 사용하여 소프트 삭제를 구현한 것이 좋습니다. BaseTimeEntity를 상속받아 타임스탬프 관리도 적절히 처리되고 있습니다.


49-74: 도메인 모델과의 변환 로직이 잘 구현됨

toDomain()fromDomain() 메서드를 통해 엔티티와 도메인 모델 간의 변환이 적절히 구현되어 있습니다. ReadingRecord.reconstruct()를 사용하여 도메인 객체를 재구성하는 것도 올바른 접근입니다.


33-34: 감정 태그 문자열 길이 제한 일관성 확인됨

DTO(CreateReadingRecordRequest)의 @Size(max = 10)와 Entity(ReadingRecordEntity)의 @Column(length = 10)가 일치합니다. 추가 수정이 필요 없습니다.

apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordService.kt (3)

12-15: 적절한 애플리케이션 서비스 구조

@Service 어노테이션을 사용하고 도메인 서비스에 의존성을 주입받는 구조가 잘 설계되어 있습니다. 애플리케이션 레이어의 책임(DTO 변환, 검증)과 도메인 레이어의 책임이 적절히 분리되어 있습니다.


17-30: 요청 검증과 도메인 위임 로직이 잘 구현됨

request.validXXX() 메서드들을 통해 입력값을 검증한 후 도메인 서비스에 위임하고, 결과를 응답 DTO로 변환하는 흐름이 올바르게 구현되어 있습니다.


33-40: 페이징 처리 로직이 적절함

도메인 서비스로부터 페이징된 결과를 받아 각 요소를 응답 DTO로 변환하는 로직이 올바르게 구현되어 있습니다. page.map을 사용한 변환도 적절합니다.

apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordController.kt (2)

25-36: 독서 기록 생성 엔드포인트 구현이 적절합니다.

HTTP 201 Created 상태 코드 사용, 입력 검증(@Valid), 그리고 UseCase로의 적절한 위임이 잘 구현되어 있습니다.


38-52: 도메인 계층에서 sort 파라미터 병합 로직 확인 필요
Controller → UseCase → Service 까진 sort: String?Pageable.sort를 그대로 넘기고 있고, 실제 정렬 로직은 도메인 서비스(findReadingRecordsByDynamicCondition)에서 처리됩니다.
도메인 모듈 구현체에서 아래 사항을 반드시 검증해주세요:

  • sort 파라미터가 null일 때 PageableDefault(createdAt DESC)가 적용되는지
  • sort 파라미터가 주어졌을 때 올바르게 오버라이드 되는지
  • 두 정렬 옵션 병합 시 우선순위와 인젝션 방식에 문제는 없는지

(도메인 모듈의 ReadingRecordDomainService 구현체 위치를 찾아 위 로직을 직접 검토해 주세요.)

apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCase.kt (2)

15-21: UseCase 클래스 구성이 적절합니다.

@UseCase 어노테이션과 기본 읽기 전용 트랜잭션 설정, 그리고 필요한 서비스들의 의존성 주입이 잘 구현되어 있습니다.


22-35: 독서 기록 생성 로직이 올바르게 구현되어 있습니다.

사용자 존재 검증 → 사용자 도서 존재 검증 → 독서 기록 생성의 순서가 적절하며, 쓰기 트랜잭션(@Transactional) 적용도 올바릅니다.

infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/ReadingRecordRepositoryImpl.kt (4)

13-21: 도메인-엔티티 변환 로직이 적절하게 구현되어 있습니다.

saveAndFlush 사용으로 즉시 데이터베이스에 반영하고, 도메인 객체 변환이 올바르게 수행되고 있습니다.


23-25: null 안전성을 고려한 조회 로직이 잘 구현되어 있습니다.

findByIdOrNull 사용과 null 처리가 적절합니다.


27-40: 컬렉션 조회 메서드들이 올바르게 구현되어 있습니다.

단일 및 복수 userBookId 조회, 페이징 처리 모두 적절하게 도메인 객체로 변환하고 있습니다.


46-53: 동적 조건 조회 메서드 구현이 적절합니다.

JPA repository의 동적 조건 조회를 위임하고 결과를 도메인 객체로 변환하는 로직이 올바릅니다.

apis/src/main/kotlin/org/yapp/apis/book/service/UserBookService.kt (4)

10-11: 새로운 예외 처리 클래스 import가 적절합니다.

UserBookErrorCodeUserBookNotFoundException import로 표준화된 오류 처리가 가능해졌습니다.


35-41: 사용자 도서 존재 검증 메서드가 잘 구현되어 있습니다.

사용자 ID와 도서 ID 모두를 검증하고, 적절한 예외 메시지와 함께 UserBookNotFoundException을 발생시키는 로직이 올바릅니다.


22-33: 리팩터링된 upsertUserBook 메서드의 가독성이 개선되었습니다.

중간 변수 userBookInfoVO 사용으로 코드 가독성이 향상되었습니다.


44-50: findAllByUserIdAndBookIsbnIn 메서드 리팩터링이 적절합니다.

중간 변수 userBooks 사용으로 코드가 더 명확해졌습니다.

domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecord.kt (7)

7-17: 도메인 엔티티 설계가 잘 구성되어 있습니다.

private 생성자와 불변성, 그리고 타입 안전성을 위한 value class 사용이 적절합니다.


19-34: create 팩토리 메서드가 올바르게 구현되어 있습니다.

UUID 생성과 value object 생성을 통한 새로운 도메인 객체 생성 로직이 적절합니다.


36-59: reconstruct 팩토리 메서드가 데이터 재구성에 적합합니다.

영속화된 데이터로부터 도메인 객체를 재구성하는 로직이 잘 구현되어 있습니다.


75-83: PageNumber value class의 검증 로직이 적절합니다.

1-9999 범위 검증으로 현실적인 페이지 번호 제약을 적용하고 있습니다.


85-94: Quote value class의 검증 로직이 적절합니다.

공백 검증과 1000자 제한이 인용구 데이터에 적합합니다.


96-105: Review value class의 검증 로직이 적절합니다.

공백 검증과 1000자 제한이 리뷰 데이터에 적합합니다.


107-117: EmotionTag value class의 검증 로직이 적절합니다.

공백 검증과 10자 제한이 감정 태그에 적합한 제약입니다.

apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerApi.kt (2)

28-30: 인터페이스 선언이 잘 구성되었습니다.

RESTful 설계 원칙을 잘 따르고 있으며, Swagger 문서화도 적절합니다.


32-60: 독서 기록 생성 메서드가 잘 구현되었습니다.

적절한 인증 처리, 유효성 검증, API 문서화가 포함되어 있습니다.

Comment on lines 32 to 54
companion object {
fun of(
query: String,
queryType: String? = null,
searchTarget: String? = null,
maxResults: Int? = null,
start: Int? = null,
sort: String? = null,
cover: String? = null,
categoryId: Int? = null
): BookSearchRequest {
return BookSearchRequest(
query = query,
queryType = queryType,
searchTarget = searchTarget,
maxResults = maxResults,
start = start,
sort = sort,
cover = cover,
categoryId = categoryId
)
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

팩토리 메서드 구현이 잘 되었지만 nullability 일관성을 개선할 수 있습니다.

팩토리 메서드 of를 통한 객체 생성 제어는 잘 구현되었습니다. 하지만 다음과 같은 개선점이 있습니다:

  1. 팩토리 메서드에서는 query를 non-null로 받지만, 생성자에서는 nullable로 정의되어 있어 일관성이 부족합니다.
  2. validQuery() 메서드가 null assertion(!!)을 사용하는데, 생성자가 nullable을 허용하면 런타임 예외 위험이 있습니다.

다음과 같이 개선하는 것을 권장합니다:

-data class BookSearchRequest private constructor(
-    val query: String? = null,
+data class BookSearchRequest private constructor(
+    val query: String,
     val queryType: String? = null,
     val searchTarget: String? = null,
     val maxResults: Int? = null,
     val start: Int? = null,
     val sort: String? = null,
     val cover: String? = null,
     val categoryId: Int? = null
 ) {

-    fun validQuery(): String = query!!
+    fun validQuery(): String = query

또는 팩토리 메서드에서 validation을 수행하도록 개선할 수도 있습니다:

companion object {
    fun of(
        query: String,
        // ... other parameters
    ): BookSearchRequest {
        require(query.isNotBlank()) { "Query must not be blank" }
        return BookSearchRequest(
            query = query,
            // ... other parameters
        )
    }
}
🤖 Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookSearchRequest.kt
between lines 32 and 54, the factory method 'of' accepts a non-null 'query'
parameter but the class constructor allows 'query' to be nullable, causing
inconsistency and potential runtime exceptions when 'validQuery()' uses null
assertions. To fix this, update the class constructor to make 'query'
non-nullable to match the factory method, or alternatively, add validation
inside the 'of' method to ensure 'query' is not blank or null before creating
the object, preventing null-related runtime errors.

Comment on lines 31 to 34
fun validPageNumber(): Int = pageNumber!!
fun validQuote(): String = quote!!
fun validReview(): String = review!!
fun validEmotionTags(): List<String> = emotionTags!!
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Non-null assertion 연산자 사용의 안전성 개선

validXXX() 메서드들에서 !! 연산자를 사용하고 있습니다. 검증이 통과한 후 호출된다는 가정하에는 안전하지만, 더 명시적인 방법을 고려해볼 수 있습니다.

fun validPageNumber(): Int = pageNumber ?: throw IllegalStateException("페이지 번호가 검증되지 않았습니다")
fun validQuote(): String = quote ?: throw IllegalStateException("기억에 남는 문장이 검증되지 않았습니다")
fun validReview(): String = review ?: throw IllegalStateException("감상평이 검증되지 않았습니다")
fun validEmotionTags(): List<String> = emotionTags ?: throw IllegalStateException("감정 태그가 검증되지 않았습니다")
🤖 Prompt for AI Agents
In
apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequest.kt
lines 31 to 34, replace the non-null assertion operator (!!) in the validXXX()
methods with explicit null checks that throw IllegalStateException with
descriptive messages if the properties are null. This improves safety by clearly
handling unexpected null cases instead of risking a NullPointerException.

Comment on lines 37 to 52
fun getReadingRecordsByUserBookId(
userId: UUID,
userBookId: UUID,
sort: String?,
pageable: Pageable
): Page<ReadingRecordResponse> {
userAuthService.validateUserExists(userId)

userBookService.validateUserBookExists(userId, userBookId)

return readingRecordService.getReadingRecordsByDynamicCondition(
userBookId = userBookId,
sort = sort,
pageable = pageable
)
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

독서 기록 조회 로직에서 검증 순서를 개선할 수 있습니다.

현재 사용자 검증과 사용자 도서 검증을 별도로 수행하고 있는데, validateUserBookExists에서 이미 사용자와 도서의 연관성을 검증하므로 사용자 검증을 중복으로 수행할 필요가 있는지 검토해보세요.

다음과 같이 리팩터링을 고려해볼 수 있습니다:

 fun getReadingRecordsByUserBookId(
     userId: UUID,
     userBookId: UUID,
     sort: String?,
     pageable: Pageable
 ): Page<ReadingRecordResponse> {
-    userAuthService.validateUserExists(userId)
-
     userBookService.validateUserBookExists(userId, userBookId)

     return readingRecordService.getReadingRecordsByDynamicCondition(
         userBookId = userBookId,
         sort = sort,
         pageable = pageable
     )
 }
📝 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
fun getReadingRecordsByUserBookId(
userId: UUID,
userBookId: UUID,
sort: String?,
pageable: Pageable
): Page<ReadingRecordResponse> {
userAuthService.validateUserExists(userId)
userBookService.validateUserBookExists(userId, userBookId)
return readingRecordService.getReadingRecordsByDynamicCondition(
userBookId = userBookId,
sort = sort,
pageable = pageable
)
}
fun getReadingRecordsByUserBookId(
userId: UUID,
userBookId: UUID,
sort: String?,
pageable: Pageable
): Page<ReadingRecordResponse> {
userBookService.validateUserBookExists(userId, userBookId)
return readingRecordService.getReadingRecordsByDynamicCondition(
userBookId = userBookId,
sort = sort,
pageable = pageable
)
}
🤖 Prompt for AI Agents
In
apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCase.kt
between lines 37 and 52, the current code validates the user existence first and
then validates the user book existence, but since validateUserBookExists already
checks the association between user and book, the initial user existence
validation is redundant. Remove the separate userAuthService.validateUserExists
call and rely solely on userBookService.validateUserBookExists to perform both
validations, simplifying the logic and avoiding duplicate checks.

Comment on lines 24 to 47
pageNumber: Int,
quote: String,
review: String,

@ElementCollection
@CollectionTable(
name = "reading_record_emotion_tags",
joinColumns = [JoinColumn(name = "reading_record_id")]
)
@Column(name = "tag", nullable = false, length = 10)
val emotionTags: List<String> = emptyList()
) : 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
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

생성자 파라미터와 프로퍼티 재정의의 혼란

생성자에서 pageNumber, quote, review를 일반 파라미터로 받은 후, 클래스 내부에서 같은 이름의 var 프로퍼티로 재정의하고 있습니다. 이는 코드 가독성을 해칠 수 있습니다.

더 명확한 구조로 개선하는 것을 권장합니다:

@Entity
@Table(name = "reading_records")
@SQLDelete(sql = "UPDATE reading_records SET deleted_at = NOW() WHERE id = ?")
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,

    @Column(name = "page_number", nullable = false)
    var pageNumber: Int,

    @Column(name = "quote", nullable = false, length = 1000)
    var quote: String,

    @Column(name = "review", nullable = false, length = 1000)
    var review: String,

    @ElementCollection
    @CollectionTable(
        name = "reading_record_emotion_tags",
        joinColumns = [JoinColumn(name = "reading_record_id")]
    )
    @Column(name = "tag", nullable = false, length = 10)
    val emotionTags: List<String> = emptyList()
) : BaseTimeEntity() {
    
    init {
        // protected setter 효과를 위해 필요한 경우에만 setter 제한
    }
}
🤖 Prompt for AI Agents
In
infra/src/main/kotlin/org/yapp/infra/readingrecord/entity/ReadingRecordEntity.kt
around lines 24 to 47, the constructor parameters pageNumber, quote, and review
are declared as plain parameters and then redefined as var properties inside the
class, which reduces code clarity. To fix this, declare these as var properties
directly in the primary constructor with the appropriate @Column annotations,
removing the redundant internal property declarations and their setters. This
will simplify the class structure and improve readability.

Copy link
Member

@move-hoon move-hoon left a comment

Choose a reason for hiding this comment

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

고생하셨습니다 민우님!!

몇가지 코멘트 남겼습니다. 정말정말 감사합니다

Comment on lines 32 to 54
companion object {
fun of(
query: String,
queryType: String? = null,
searchTarget: String? = null,
maxResults: Int? = null,
start: Int? = null,
sort: String? = null,
cover: String? = null,
categoryId: Int? = null
): BookSearchRequest {
return BookSearchRequest(
query = query,
queryType = queryType,
searchTarget = searchTarget,
maxResults = maxResults,
start = start,
sort = sort,
cover = cover,
categoryId = categoryId
)
}
}
Copy link
Member

Choose a reason for hiding this comment

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

해당 정적 팩토리 메서드는 사용중이지 않은데 만들게 되신 이유가 궁금합니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

아 해당 정적 팩토리 메소드는 필요 없을 것 같네요 삭제할게요~

import java.util.UUID
import jakarta.validation.Valid

@RestController
Copy link
Member

Choose a reason for hiding this comment

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

여기에 RequestMapping 추가해주시면 감사하겠습니다! (가독성을 위해서)

Comment on lines 25 to 52
override fun createReadingRecord(
@AuthenticationPrincipal userId: UUID,
@PathVariable userBookId: UUID,
@Valid @RequestBody request: CreateReadingRecordRequest
): ResponseEntity<ReadingRecordResponse> {
val response = readingRecordUseCase.createReadingRecord(
userId = userId,
userBookId = userBookId,
request = request
)
return ResponseEntity.status(HttpStatus.CREATED).body(response)
}

override fun getReadingRecords(
@AuthenticationPrincipal userId: UUID,
@PathVariable userBookId: UUID,
@RequestParam(required = false) sort: String?,
@PageableDefault(size = 10, sort = ["createdAt"], direction = Sort.Direction.DESC)
pageable: Pageable
): ResponseEntity<Page<ReadingRecordResponse>> {
val response = readingRecordUseCase.getReadingRecordsByUserBookId(
userId = userId,
userBookId = userBookId,
sort = sort,
pageable = pageable
)
return ResponseEntity.ok(response)
}
Copy link
Member

Choose a reason for hiding this comment

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

그리고 getmapping이랑 postmapping도 추가해주시면 감사할 거 같아요 ㅎㅎ (이것도 가독성 때문에 ㅠㅠ)

import java.util.UUID

@Tag(name = "Reading Records", description = "독서 기록 관련 API")
@RequestMapping("/api/v1/user-books/{userBookId}/reading-records")
Copy link
Member

Choose a reason for hiding this comment

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

/api/v1/reading-records로 경로를 바꾼다면 더 의미가 살아날 거 같습니다! (개인적인 생각)

그리고 requestmapping에서 pathvariable 설정까지 하게되면 추후 다른 독서 기록 관련 api에서 userBookId가 필요하지 않는 상황이 온다면 경로를 수정해야 하는 상황이 생길 것 같다는 생각이 들었습니다!!

제가 추천한 경로대로 간다면,

@RequestMapping("/api/v1/reading-records)
@PostMapping("/{userBookId}")

이런식으로 진행하면 좋을 거 같아요!

Comment on lines 36 to 51
companion object {
fun of(
pageNumber: Int,
quote: String,
review: String,
emotionTags: List<String>
): CreateReadingRecordRequest {
return CreateReadingRecordRequest(
pageNumber = pageNumber,
quote = quote,
review = review,
emotionTags = emotionTags
)
}
}
}
Copy link
Member

Choose a reason for hiding this comment

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

여기서도 정팩메가 안쓰여서 삭제해도 될 것 같다는 생각이 듭니다!!

Copy link
Member Author

Choose a reason for hiding this comment

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

네네 여기도 정팩메 안쓰니까 삭제할게요!

Comment on lines 28 to 34
@ElementCollection
@CollectionTable(
name = "reading_record_emotion_tags",
joinColumns = [JoinColumn(name = "reading_record_id")]
)
@Column(name = "tag", nullable = false, length = 10)
val emotionTags: List<String> = emptyList()
Copy link
Member

Choose a reason for hiding this comment

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

@ElementCollection도 외래 키를 생성하는 것으로 알고 있습니다.
하지만 값 타입 컬렉션을 저장하기에 적합한 기능이기 때문에, 별도의 테이블을 정의하고 식별자를 부여하는 방식으로 구현할지, 아니면 @ElementCollection을 사용하여 간단하게 매핑할지 고민이 됩니다!!

이 부분에 대해서 민우님의 생각이 궁금합니다 ㅎㅎ

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 78e9e6c and c4db4f4.

📒 Files selected for processing (2)
  • apis/src/main/kotlin/org/yapp/apis/book/service/UserBookService.kt (2 hunks)
  • domain/src/main/kotlin/org/yapp/domain/userbook/UserBookDomainService.kt (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-validation
🔇 Additional comments (2)
domain/src/main/kotlin/org/yapp/domain/userbook/UserBookDomainService.kt (1)

73-75: 새로운 조회 메서드 구현이 적절합니다.

findByIdAndUserId 메서드의 구현이 간결하고 명확합니다. nullable 반환 타입을 통해 존재하지 않는 경우를 적절히 처리하고 있으며, 도메인 서비스에서 repository로의 위임 패턴도 올바릅니다.

파라미터 순서(userBookId, userId)가 일관성 있게 사용되고 있어 좋습니다.

apis/src/main/kotlin/org/yapp/apis/book/service/UserBookService.kt (1)

10-11: 새로운 예외 처리 import가 적절합니다.

UserBookErrorCodeUserBookNotFoundException import가 새로운 검증 기능에 필요한 예외 처리를 위해 적절히 추가되었습니다.

Comment on lines +35 to 41
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"
)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

예외 메시지에서 민감한 정보 노출을 고려해주세요.

메서드 구현은 적절하지만 다음 사항들을 검토해보시기 바랍니다:

  1. 보안 고려사항: 예외 메시지에 userBookIduserId가 직접 노출되고 있습니다. 로그나 클라이언트 응답에서 이러한 내부 ID가 노출될 수 있습니다.
  2. 파라미터 순서: 도메인 서비스의 findByIdAndUserId(userBookId, userId)와 달리 현재 메서드는 (userId, userBookId) 순서를 사용합니다. 일관성을 위해 순서를 맞추는 것을 고려해보세요.

다음과 같이 수정을 제안합니다:

-    fun validateUserBookExists(userId: UUID, userBookId: UUID): UserBook {
-        return userBookDomainService.findByIdAndUserId(userBookId, userId)
+    fun validateUserBookExists(userBookId: UUID, userId: UUID): UserBook {
+        return userBookDomainService.findByIdAndUserId(userBookId, userId)
             ?: throw UserBookNotFoundException(
                 UserBookErrorCode.USER_BOOK_NOT_FOUND,
-                "User book not found with id: $userBookId and userId: $userId"
+                "User book not found"
             )
     }
📝 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
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 validateUserBookExists(userBookId: UUID, userId: UUID): UserBook {
return userBookDomainService.findByIdAndUserId(userBookId, userId)
?: throw UserBookNotFoundException(
UserBookErrorCode.USER_BOOK_NOT_FOUND,
"User book not found"
)
}
🤖 Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/book/service/UserBookService.kt around
lines 35 to 41, the exception message exposes sensitive internal IDs (userBookId
and userId), which should be avoided for security reasons. Modify the exception
message to exclude these IDs or replace them with a generic message. Also,
adjust the parameter order in the validateUserBookExists method to match the
domain service's findByIdAndUserId method by passing userBookId first and then
userId for consistency.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

♻️ Duplicate comments (8)
infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/JpaReadingRecordQuerydslRepository.kt (1)

9-16: 타입 안전성이 개선된 좋은 구현입니다.

이전 리뷰에서 제안된 ReadingRecordSortType enum을 사용하여 정렬 파라미터의 타입 안전성을 확보했습니다. 런타임에 잘못된 정렬 기준이 전달될 위험이 제거되었습니다.

apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequest.kt (1)

31-34: Non-null assertion 연산자 사용 개선 필요

이전 리뷰에서도 언급된 바와 같이, validXXX() 메서드들에서 !! 연산자 대신 명시적인 null 체크와 예외 처리를 사용하는 것이 더 안전합니다.

domain/src/main/kotlin/org/yapp/domain/readingrecord/vo/ReadingRecordInfoVO.kt (1)

18-20: 타임스탬프 검증 로직 개선

이전 리뷰에서 제안된 바와 같이, !createdAt.isAfter(updatedAt) 사용이 현재 조건보다 더 명확하고 간결합니다.

domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordRepository.kt (2)

3-4: 도메인 레이어의 인프라스트럭처 의존성

도메인 레이어에서 Spring Data의 PagePageable을 직접 사용하고 있습니다. DDD 관점에서 도메인은 인프라스트럭처에 의존하지 않아야 합니다.

도메인 전용 페이징 인터페이스를 정의하거나, 이 의존성을 애플리케이션 레이어로 이동시키는 것을 고려해보세요.


16-19: 메서드 오버로딩으로 인한 혼란 가능성

findAllByUserBookId 메서드가 두 개의 서로 다른 반환 타입(List<ReadingRecord>, Page<ReadingRecord>)을 가지고 있습니다. 이는 호출하는 쪽에서 혼란을 야기할 수 있습니다.

메서드명을 더 명확하게 구분하는 것을 권장합니다:

- fun findAllByUserBookId(userBookId: UUID): List<ReadingRecord>
- fun findAllByUserBookId(userBookId: UUID, pageable: Pageable): Page<ReadingRecord>
+ fun findAllByUserBookId(userBookId: UUID): List<ReadingRecord>
+ fun findAllByUserBookIdWithPagination(userBookId: UUID, pageable: Pageable): Page<ReadingRecord>
infra/src/main/kotlin/org/yapp/infra/readingrecord/entity/ReadingRecordEntity.kt (1)

26-43: 생성자 파라미터와 프로퍼티 재정의의 혼란

생성자에서 pageNumber, quote, review를 일반 파라미터로 받은 후, 클래스 내부에서 같은 이름의 var 프로퍼티로 재정의하고 있습니다. 이는 코드 가독성을 해칠 수 있습니다.

더 명확한 구조로 개선하는 것을 권장합니다:

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,
+    @Column(name = "page_number", nullable = false)
+    var pageNumber: Int,
+
+    @Column(name = "quote", nullable = false, length = 1000)
+    var quote: String,
+
+    @Column(name = "review", nullable = false, length = 1000)
+    var 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
apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCase.kt (1)

35-36: 중복된 사용자 검증 로직을 제거해주세요.

userBookService.validateUserBookExists에서 이미 사용자-도서 연관성을 검증하므로 별도의 userAuthService.validateUserExists 호출이 불필요합니다.

다음과 같이 수정하여 중복 검증을 제거할 수 있습니다:

 fun createReadingRecord(
     userId: UUID,
     userBookId: UUID,
     request: CreateReadingRecordRequest
 ): ReadingRecordResponse {
-    userAuthService.validateUserExists(userId)
     userBookService.validateUserBookExists(userId, userBookId)
     
     return readingRecordService.createReadingRecord(
 fun getReadingRecordsByUserBookId(
     userId: UUID,
     userBookId: UUID,
     sort: ReadingRecordSortType?,
     pageable: Pageable
 ): Page<ReadingRecordResponse> {
-    userAuthService.validateUserExists(userId)
-
     userBookService.validateUserBookExists(userId, userBookId)

Also applies to: 51-53

apis/src/main/kotlin/org/yapp/apis/book/service/UserBookService.kt (1)

37-43: 예외 메시지에서 민감한 정보 노출을 방지하고 매개변수 순서를 통일해주세요.

예외 메시지에 userBookIduserId가 직접 노출되고 있으며, 도메인 서비스와 매개변수 순서가 일치하지 않습니다.

다음과 같이 수정을 제안합니다:

- fun validateUserBookExists(userId: UUID, userBookId: UUID): UserBook {
-     return userBookDomainService.findByIdAndUserId(userBookId, userId)
+ fun validateUserBookExists(userBookId: UUID, userId: UUID): UserBook {
+     return userBookDomainService.findByIdAndUserId(userBookId, userId)
          ?: throw UserBookNotFoundException(
              UserBookErrorCode.USER_BOOK_NOT_FOUND,
-             "User book not found with id: $userBookId and userId: $userId"
+             "User book not found"
          )
  }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c4db4f4 and 49104b6.

📒 Files selected for processing (25)
  • apis/src/main/kotlin/org/yapp/apis/book/controller/BookController.kt (2 hunks)
  • apis/src/main/kotlin/org/yapp/apis/book/controller/BookControllerApi.kt (2 hunks)
  • apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookDetailResponse.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/book/service/UserBookService.kt (5 hunks)
  • apis/src/main/kotlin/org/yapp/apis/book/usecase/BookUseCase.kt (3 hunks)
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordController.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerApi.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequest.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/ReadingRecordResponse.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordService.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCase.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordRepository.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordSortType.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/readingrecord/vo/ReadingRecordInfoVO.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/userbook/UserBookDomainService.kt (4 hunks)
  • domain/src/main/kotlin/org/yapp/domain/userbook/UserBookRepository.kt (2 hunks)
  • domain/src/main/kotlin/org/yapp/domain/userbook/UserBookSortType.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/userbook/vo/UserBookInfoVO.kt (3 hunks)
  • infra/src/main/kotlin/org/yapp/infra/readingrecord/entity/ReadingRecordEntity.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/JpaReadingRecordQuerydslRepository.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/JpaReadingRecordQuerydslRepositoryImpl.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/ReadingRecordRepositoryImpl.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/infra/userbook/repository/JpaUserBookQuerydslRepository.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/infra/userbook/repository/impl/JpaUserBookQuerydslRepositoryImpl.kt (4 hunks)
  • infra/src/main/kotlin/org/yapp/infra/userbook/repository/impl/UserBookRepositoryImpl.kt (3 hunks)
🧰 Additional context used
🧠 Learnings (2)
apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/ReadingRecordResponse.kt (1)

Learnt from: hoonyworld
PR: #39
File: apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookSearchResponse.kt:45-45
Timestamp: 2025-07-17T15:09:36.267Z
Learning: Response DTO 클래스에서는 불변성을 지키기 위해 프로퍼티를 val로 선언하고, 업데이트가 필요한 경우 copy() 메서드를 사용해야 합니다.

domain/src/main/kotlin/org/yapp/domain/readingrecord/vo/ReadingRecordInfoVO.kt (1)

Learnt from: hoonyworld
PR: #39
File: apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookSearchResponse.kt:45-45
Timestamp: 2025-07-17T15:09:36.267Z
Learning: Response DTO 클래스에서는 불변성을 지키기 위해 프로퍼티를 val로 선언하고, 업데이트가 필요한 경우 copy() 메서드를 사용해야 합니다.

🧬 Code Graph Analysis (1)
infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/JpaReadingRecordQuerydslRepositoryImpl.kt (1)
infra/src/main/kotlin/org/yapp/infra/userbook/repository/impl/JpaUserBookQuerydslRepositoryImpl.kt (1)
  • createOrderSpecifier (73-81)
🪛 GitHub Actions: CI - Pull Request
domain/src/main/kotlin/org/yapp/domain/userbook/vo/UserBookInfoVO.kt

[error] 10-10: Unresolved reference: 'BookId'.


[error] 37-37: Unresolved reference: 'bookId'.


[error] Task ':domain:compileKotlin' failed due to compilation errors.

domain/src/main/kotlin/org/yapp/domain/userbook/UserBookSortType.kt

[error] Task ':domain:compileKotlin' failed due to compilation errors.

domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordSortType.kt

[error] Task ':domain:compileKotlin' failed due to compilation errors.

domain/src/main/kotlin/org/yapp/domain/userbook/UserBookDomainService.kt

[error] 27-27: Cannot find a parameter with this name: 'bookId'.


[error] Task ':domain:compileKotlin' failed due to compilation errors.

domain/src/main/kotlin/org/yapp/domain/userbook/UserBookRepository.kt

[error] Task ':domain:compileKotlin' failed due to compilation errors.

domain/src/main/kotlin/org/yapp/domain/readingrecord/vo/ReadingRecordInfoVO.kt

[error] Task ':domain:compileKotlin' failed due to compilation errors.

domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordRepository.kt

[error] Task ':domain:compileKotlin' failed due to compilation errors.

🔇 Additional comments (43)
domain/src/main/kotlin/org/yapp/domain/userbook/UserBookSortType.kt (1)

4-9: enum 클래스 구현이 적절합니다.

정렬 타입을 나타내는 enum 클래스가 명확하게 정의되었습니다. 네이밍 컨벤션도 일관성 있고 이해하기 쉽습니다.

apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookDetailResponse.kt (1)

28-28: trailing comma 추가가 적절합니다.

Kotlin의 trailing comma 사용은 향후 프로퍼티 추가 시 diff를 깔끔하게 만들어주는 좋은 프랙티스입니다.

domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordSortType.kt (1)

3-8: 독서 기록 정렬 타입이 적절하게 정의되었습니다.

UserBookSortType과 일관된 패턴을 따르고 있으며, 독서 기록 도메인에 적합한 정렬 옵션들이 제공됩니다.

infra/src/main/kotlin/org/yapp/infra/userbook/repository/JpaUserBookQuerydslRepository.kt (2)

6-6: import 추가가 적절합니다.

UserBookSortType enum 사용을 위한 필요한 import입니다.


14-14: 타입 안전성이 향상되었습니다.

sort 파라미터를 String?에서 UserBookSortType?으로 변경하여 컴파일 타임에 잘못된 정렬 값을 검증할 수 있게 되었습니다.

domain/src/main/kotlin/org/yapp/domain/userbook/vo/UserBookInfoVO.kt (1)

25-27: 시간 검증 로직이 간소화되어 좋습니다.

createdAt이 updatedAt보다 이후가 아니어야 한다는 검증 로직이 명확하고 적절합니다.

apis/src/main/kotlin/org/yapp/apis/book/usecase/BookUseCase.kt (3)

23-23: 도메인 특화 타입 사용으로 타입 안전성 향상.

UserBookSortType enum 도입으로 정렬 파라미터의 타입 안전성이 개선되었습니다.


67-67: 코드 스타일 개선.

trailing comma 추가로 향후 diff 가독성이 향상됩니다.


77-77: 일관된 타입 안전성 적용.

API 계층 전반에 걸쳐 UserBookSortType enum이 일관되게 적용되어 타입 안전성이 확보되었습니다.

apis/src/main/kotlin/org/yapp/apis/book/controller/BookControllerApi.kt (2)

30-30: 도메인 타입 import 추가.

API 인터페이스에서 도메인 특화 enum 사용을 위한 적절한 import입니다.


144-144: API 계층의 타입 안전성 개선.

정렬 파라미터에 도메인 특화 enum을 사용하여 API 레벨에서의 타입 안전성이 향상되었습니다. nullable 타입을 유지하여 하위 호환성도 확보했습니다.

apis/src/main/kotlin/org/yapp/apis/book/controller/BookController.kt (2)

25-25: 컨트롤러 구현의 타입 안전성 지원.

API 인터페이스와 일관된 도메인 enum 사용을 위한 적절한 import입니다.


65-65: 인터페이스와 구현의 일관성 확보.

API 인터페이스와 동일한 타입을 사용하여 일관성을 유지하면서 타입 안전성을 확보했습니다.

apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/ReadingRecordResponse.kt (3)

10-10: 적절한 생성자 접근 제어.

private 생성자와 companion object 팩토리 메서드 패턴을 사용하여 객체 생성을 제어하고 있습니다.


11-33: 불변성을 지킨 Response DTO 설계.

검색된 학습 내용에 따라 Response DTO에서 모든 프로퍼티를 val로 선언하여 불변성을 올바르게 지키고 있습니다. Swagger 문서화도 예시와 함께 적절히 작성되었습니다.


35-50: 도메인과 API 계층 간의 적절한 분리.

팩토리 메서드를 통해 도메인 VO에서 API Response DTO로의 변환을 깔끔하게 처리하고 있습니다. 날짜 포맷팅도 일관성 있게 적용되었습니다.

infra/src/main/kotlin/org/yapp/infra/userbook/repository/impl/JpaUserBookQuerydslRepositoryImpl.kt (3)

11-11: 타입 안전성 개선을 위한 enum 도입이 좋습니다.

UserBookSortType enum을 도입하여 문자열 기반 정렬에서 타입 안전한 정렬로 개선한 점이 훌륭합니다.


27-27: enum 기반 정렬 로직이 올바르게 구현되었습니다.

UserBookSortType enum을 사용하여 정렬 로직을 구현한 부분이 적절합니다. null 처리도 기본값(CREATED_DATE_DESC)으로 잘 처리되어 있습니다.

Also applies to: 73-80


44-50: 쿼리 포맷팅 변경사항 확인됨

count 쿼리의 포맷팅이 변경되었지만 기능상 문제없습니다. 가독성 개선을 위한 변경으로 보입니다.

domain/src/main/kotlin/org/yapp/domain/userbook/UserBookRepository.kt (2)

11-12: 사용자 검증을 위한 새로운 쿼리 메서드가 잘 추가되었습니다.

findByBookIdAndUserIdfindByIdAndUserId 메서드 추가로 사용자별 UserBook 검증 기능이 강화되었습니다. 이는 보안과 데이터 무결성 측면에서 중요한 개선사항입니다.


23-23: UserBookSortType import 구문 불필요 확인
UserBookSortType.kt와 UserBookRepository.kt는 동일 패키지(org.yapp.domain.userbook)에 위치하므로, import 없이도 타입 참조가 가능합니다. 해당 파일에 import를 추가할 필요가 없습니다.

Likely an incorrect or invalid review comment.

apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequest.kt (1)

10-35: 독서 기록 요청 DTO 구조가 잘 설계되었습니다.

적절한 validation 어노테이션과 Swagger 문서화, private constructor 패턴 사용으로 안전하고 명확한 API 인터페이스를 제공하고 있습니다.

domain/src/main/kotlin/org/yapp/domain/readingrecord/vo/ReadingRecordInfoVO.kt (2)

6-21: 불변 값 객체가 잘 설계되었습니다.

Retrieved learning에 따르면 Response DTO에서는 불변성을 위해 val 프로퍼티를 사용해야 하는데, 이 VO가 그 원칙을 잘 따르고 있습니다. private constructor와 validation 로직도 적절합니다.


23-39: 팩토리 메서드의 null 체크가 적절합니다.

newInstance 메서드에서 timestamp null 체크와 명시적인 예외 메시지가 잘 구현되어 있습니다. 도메인 객체에서 VO로의 변환 로직이 안전하게 처리되고 있습니다.

infra/src/main/kotlin/org/yapp/infra/userbook/repository/impl/UserBookRepositoryImpl.kt (3)

9-9: 타입 안전성을 위한 import 추가가 적절합니다.

UserBookSortType import 추가로 문자열 기반에서 enum 기반 정렬로 개선되었습니다.


23-29: 새로운 쿼리 메서드들이 올바르게 구현되었습니다.

findByBookIdAndUserIdfindByIdAndUserId 메서드가 기존 패턴과 일관성 있게 구현되었습니다. JPA repository 위임과 도메인 객체 변환 로직이 적절합니다.


51-51: 정렬 파라미터 타입 변경이 일관성 있게 적용되었습니다.

UserBookSortType?으로 타입 변경이 도메인 레이어와 일관성 있게 적용되어 타입 안전성이 향상되었습니다.

infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/JpaReadingRecordQuerydslRepositoryImpl.kt (1)

16-57: 구현이 잘 되어 있습니다.

이전 리뷰 의견이 반영되어 whereCondition을 추출하여 중복을 제거했고, 타입 안전한 정렬을 위해 ReadingRecordSortType enum을 사용하고 있습니다. UserBookQuerydslRepositoryImpl과 일관된 패턴을 따르고 있어 코드 품질이 좋습니다.

domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordRepository.kt (2)

28-32: 타입 안전성 개선이 적용되었습니다.

sort 파라미터가 ReadingRecordSortType?으로 변경되어 타입 안전성이 개선되었습니다. 이전 리뷰에서 제안한 enum 사용이 적절히 반영되었습니다.


1-33: ReadingRecordSortType enum이 이미 정의되어 있습니다

domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordSortType.kt 파일의 패키지 선언(org.yapp.domain.readingrecord) 아래에 enum class ReadingRecordSortType가 존재하므로, 해당 타입 미정의로 인한 컴파일 오류는 발생하지 않습니다. 실제 오류 로그를 다시 살펴보고 다른 원인을 검토해 보세요.

Likely an incorrect or invalid review comment.

domain/src/main/kotlin/org/yapp/domain/userbook/UserBookDomainService.kt (3)

16-16: bookId 파라미터 추가가 적절합니다.

upsertUserBook 메서드에 bookId 파라미터를 추가하여 도메인 모델을 더 풍부하게 만들었습니다.


43-43: 타입 안전성 개선이 잘 적용되었습니다.

sort 파라미터가 String?에서 UserBookSortType?으로 변경되어 타입 안전성이 크게 개선되었습니다. 런타임 오류를 방지하고 IDE 지원을 받을 수 있게 되었습니다.


69-71: 새로운 검증 메서드 추가가 적절합니다.

findByIdAndUserId 메서드 추가로 사용자별 유저북 검증 로직을 지원할 수 있게 되었습니다. ReadingRecord 기능에서 유저북 존재 여부를 확인하는 데 유용할 것입니다.

apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordService.kt (3)

16-20: 의존성 주입이 적절하게 구성되었습니다.

ReadingRecordDomainService, UserBookService, BookDomainService를 주입받아 각각의 책임에 따라 협력하는 구조가 잘 설계되었습니다.


22-39: 독서 기록 생성 로직이 잘 구현되었습니다.

사용자 책 존재 여부를 먼저 검증한 후 도메인 서비스를 통해 독서 기록을 생성하는 흐름이 적절합니다. 요청 DTO의 검증 메서드를 활용하여 안전한 값을 도메인으로 전달하고 있습니다.


42-49: 조회 로직의 구현이 깔끔합니다.

타입 안전한 정렬(ReadingRecordSortType)과 페이징을 지원하며, 도메인 객체를 응답 DTO로 변환하는 과정이 명확하게 분리되어 있습니다.

apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCase.kt (1)

19-28: 전체적인 구조가 잘 설계되었습니다.

UseCase 클래스로서의 역할이 명확하고, 트랜잭션 경계 설정과 의존성 주입이 적절합니다. 읽기 전용 트랜잭션 설정도 올바르게 되어있습니다.

apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordController.kt (2)

24-25: 이전 리뷰 피드백이 잘 반영되었습니다.

클래스 레벨의 @RequestMapping과 메서드별 @PostMapping, @GetMapping 어노테이션이 추가되어 가독성이 향상되었습니다.

Also applies to: 30-30, 44-44


26-60: REST API 구현이 우수합니다.

컨트롤러가 적절히 인터페이스를 구현하고, UseCase로 비즈니스 로직을 위임하며, HTTP 상태 코드와 검증이 올바르게 설정되어 있습니다.

infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/ReadingRecordRepositoryImpl.kt (1)

15-56: 리포지토리 구현이 견고합니다.

도메인-엔티티 간 변환이 올바르게 처리되고, null 안전성과 즉시 영속화(saveAndFlush) 처리가 적절합니다. 동적 쿼리 위임 구조도 잘 설계되었습니다.

apis/src/main/kotlin/org/yapp/apis/book/service/UserBookService.kt (2)

57-57: 정렬 매개변수의 타입 개선이 우수합니다.

String?에서 UserBookSortType?으로 변경하여 컴파일 타임 타입 안전성을 확보하고 유효하지 않은 정렬 값으로 인한 런타임 오류를 방지했습니다.

Also applies to: 67-67


23-35: 도메인 모델 개선과 예외 처리 강화가 잘 반영되었습니다.

validBookId() 매개변수 추가와 새로운 예외 처리 클래스 도입으로 도메인 모델의 일관성과 오류 처리가 개선되었습니다.

apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerApi.kt (1)

29-89: API 인터페이스 설계와 문서화가 매우 우수합니다.

OpenAPI 어노테이션을 통한 포괄적인 문서화, 적절한 HTTP 상태 코드 정의, 명확한 매개변수 설명이 잘 구성되어 있습니다. REST 규약을 올바르게 따르고 있습니다.

Comment on lines +85 to +87
@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
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

정렬 매개변수 설계가 부분적으로 개선되었으나 여전히 모호함이 남아있습니다.

ReadingRecordSortType enum 사용으로 타입 안전성은 향상되었지만, 커스텀 sort 매개변수와 Pageable의 기본 정렬이 동시에 존재하여 어떤 정렬이 우선되는지 불분명합니다.

다음 중 하나의 방식으로 개선을 권장합니다:

옵션 1: Pageable만 사용 (권장)

- sort: ReadingRecordSortType?,
- @PageableDefault(size = 10, sort = ["createdAt"], direction = Sort.Direction.DESC)
+ @PageableDefault(size = 10, sort = ["createdAt"], direction = Sort.Direction.DESC)

옵션 2: 커스텀 sort가 Pageable 정렬을 오버라이드하도록 명시
API 문서에 정렬 우선순위를 명확히 기술하고, 구현에서 이를 일관되게 처리하도록 보장해주세요.

📝 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
@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
@PageableDefault(size = 10, sort = ["createdAt"], direction = Sort.Direction.DESC)
@Parameter(description = "페이지네이션 정보 (페이지 번호, 페이지 크기, 정렬 방식)") pageable: Pageable
🤖 Prompt for AI Agents
In
apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerApi.kt
around lines 85 to 87, the sorting parameters are ambiguous because both a
custom sort parameter and Pageable's default sorting coexist without clear
precedence. To fix this, choose either to rely solely on Pageable's sorting by
removing the custom sort parameter, or explicitly implement logic where the
custom sort parameter overrides Pageable's sorting. Additionally, update the API
documentation to clearly state the sorting precedence and ensure the code
consistently enforces this behavior.

Comment on lines +21 to +28
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
) {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

사용하지 않는 의존성을 제거해주세요.

BookQueryServiceBookDomainService가 주입되고 있지만 실제로는 사용되지 않고 있습니다. 불필요한 결합도를 줄이기 위해 제거해주세요.

다음과 같이 수정을 제안합니다:

 @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
+    private val userBookService: UserBookService
 ) {
📝 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
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
) {
@UseCase
@Transactional(readOnly = true)
class ReadingRecordUseCase(
private val readingRecordService: ReadingRecordService,
private val userAuthService: UserAuthService,
private val userBookService: UserBookService
) {
🤖 Prompt for AI Agents
In
apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCase.kt
around lines 21 to 28, the dependencies BookQueryService and BookDomainService
are injected but not used anywhere in the class. To reduce unnecessary coupling,
remove these two dependencies from the constructor and any related annotations
or qualifiers.

val userBook = userBookRepository.findByUserIdAndBookIsbn(userId, bookIsbn)?.updateStatus(status)
?: UserBook.create(
userId = userId,
bookId = bookId,
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

UserBook.create 메서드의 bookId 파라미터 확인 필요

파이프라인 실패 로그를 보면 UserBook.create 메서드에서 bookId 파라미터를 찾을 수 없다는 오류가 발생하고 있습니다. UserBook 도메인 클래스의 create 메서드가 bookId 파라미터를 지원하는지 확인해주세요.

다음 스크립트로 UserBook.create 메서드 시그니처를 확인해보겠습니다:


🏁 Script executed:

#!/bin/bash
# UserBook.create 메서드 시그니처 확인
ast-grep --pattern $'class UserBook {
  $$$
  companion object {
    $$$
    fun create($$$) {
      $$$
    }
    $$$
  }
  $$$
}'

Length of output: 132


추가로 UserBook.create 메서드 시그니처를 정확히 확인하기 위해 아래 스크립트를 실행해주세요.


🏁 Script executed:

#!/bin/bash
# 프로젝트 루트에서 UserBook 클래스가 정의된 파일 검색
echo "🔍 class UserBook 파일 찾기..."
files=$(rg -l "class UserBook")
echo "Found files:"
echo "$files"

# 각 파일에서 create 메서드 시그니처 검색
echo ""
echo "🔍 create 메서드 시그니처 검색"
for file in $files; do
  echo ""
  echo "=== $file ==="
  rg -n "fun create" "$file"
done

Length of output: 8119


아래 스크립트로 domain/src/main/kotlin/org/yapp/domain/userbook/UserBook.kt 파일 내 create 메서드 시그니처(파라미터 목록 포함)를 확인해주세요.


🏁 Script executed:

#!/bin/bash
echo "🔍 UserBook.create 메서드 시그니처와 파라미터 확인"
rg -n 'fun create' domain/src/main/kotlin/org/yapp/domain/userbook/UserBook.kt -C3

Length of output: 329


UserBook.create 호출 파라미터명 불일치 확인 및 수정 필요

UserBook.create의 시그니처를 보면 bookId가 아니라 bookIsbn: String으로 정의되어 있어, 호출부에서 잘못된 파라미터 이름을 사용하고 있습니다.

수정 전 (domain/src/main/kotlin/org/yapp/domain/userbook/UserBookDomainService.kt:27)

bookId = bookId,

수정 후

bookIsbn = bookId,
  • 호출부 인자 이름을 bookIsbn으로 변경하거나, 변수명도 함께 일관되게 조정하세요.
🧰 Tools
🪛 GitHub Actions: CI - Pull Request

[error] 27-27: Cannot find a parameter with this name: 'bookId'.


[error] Task ':domain:compileKotlin' failed due to compilation errors.

🤖 Prompt for AI Agents
In domain/src/main/kotlin/org/yapp/domain/userbook/UserBookDomainService.kt at
line 27, the parameter name used in the call to UserBook.create is incorrect; it
uses bookId instead of the expected bookIsbn. Fix this by changing the argument
name from bookId to bookIsbn in the call, or alternatively rename the variable
to match the parameter name for consistency.

return UserBookInfoVO(
id = userBook.id,
userId = userBook.userId,
bookId = userBook.bookId,
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

컴파일 오류: bookId 프로퍼티가 존재하지 않습니다.

UserBook 도메인 모델에 bookId 프로퍼티가 존재하지 않아 컴파일 오류가 발생합니다. UserBook 클래스에 해당 프로퍼티를 추가하거나 올바른 프로퍼티명을 사용해야 합니다.

UserBook 도메인 모델의 프로퍼티를 확인하세요:


🏁 Script executed:

#!/bin/bash
# UserBook 클래스의 프로퍼티 확인
ast-grep --pattern 'class UserBook(
  $$$
) {
  $$$
}'

# bookId 관련 프로퍼티 검색
rg -A 3 -B 3 "bookId" domain/src/main/kotlin/org/yapp/domain/userbook/

Length of output: 3896


UserBook 도메인 모델에 bookId 프로퍼티가 정의되어 있는지 확인하기 위해 아래 스크립트로 UserBook.kt 파일 선언부와 프로퍼티 목록을 출력해주세요.


🏁 Script executed:

#!/bin/bash
# UserBook.kt 파일 경로 검색
file=$(fd UserBook.kt domain/src/main/kotlin/org/yapp/domain/userbook)
echo "Found UserBook file: $file"
# 클래스 선언부와 프로퍼티 목록 출력 (1~200행)
sed -n '1,200p' "$file"

Length of output: 3221


UserBook 도메인 모델에 bookId 프로퍼티 누락으로 인한 컴파일 오류

UserBook 클래스에는 bookId 프로퍼티와 이에 대응하는 BookId value class가 정의되어 있지 않아, VO 및 도메인 서비스에서 참조할 수 없습니다. 아래 항목을 검토하고 수정해주세요.

  • domain/src/main/kotlin/org/yapp/domain/userbook/UserBook.kt
    • primary constructor에 val bookId: BookId 추가
    @JvmInline value class BookId(val value: UUID) { companion object { fun newInstance(value: UUID) = BookId(value) } } 정의

  • domain/src/main/kotlin/org/yapp/domain/userbook/vo/UserBookInfoVO.kt
    val bookId: UserBook.BookId 선언이 실제 모델과 일치하도록 수정

  • domain/src/main/kotlin/org/yapp/domain/userbook/UserBookDomainService.kt
    UserBook.create 호출부와 시그니처에 bookId 파라미터 포함 또는 호출부에서 제거

위 변경 중 하나를 선택해 도메인 모델과 VO/서비스의 참조가 일치하도록 조정해주세요.

🧰 Tools
🪛 GitHub Actions: CI - Pull Request

[error] 37-37: Unresolved reference: 'bookId'.


[error] Task ':domain:compileKotlin' failed due to compilation errors.

🤖 Prompt for AI Agents
In domain/src/main/kotlin/org/yapp/domain/userbook/vo/UserBookInfoVO.kt at line
37 and related files, the UserBook domain model lacks the bookId property
causing compilation errors. Fix this by adding a val bookId: BookId parameter to
the primary constructor of UserBook in UserBook.kt, define the BookId value
class with a UUID property and a newInstance method, update UserBookInfoVO.kt to
declare val bookId of type UserBook.BookId, and ensure
UserBookDomainService.kt's UserBook.create method signature and calls include or
exclude bookId consistently to match the updated model.

Comment on lines +45 to +57
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
)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

emotionTags 구현이 누락되었습니다.

toDomain() 메서드에서 emotionTagsemptyList()로 하드코딩하고 있습니다. 엔티티에 emotionTags 필드가 정의되어 있지 않아 실제 데이터가 반영되지 않습니다.

@ElementCollection을 사용하여 emotion tags를 매핑하거나, 별도 엔티티로 관리하는 방식을 구현해야 합니다:

class ReadingRecordEntity(
    // ... existing fields ...
+    
+    @ElementCollection
+    @CollectionTable(
+        name = "reading_record_emotion_tags",
+        joinColumns = [JoinColumn(name = "reading_record_id")]
+    )
+    @Column(name = "tag", nullable = false, length = 10)
+    val emotionTags: List<String> = emptyList()
) : BaseTimeEntity() {

그리고 toDomain() 메서드를 수정:

fun toDomain(): ReadingRecord {
    return ReadingRecord.reconstruct(
        // ... existing fields ...
-        emotionTags = emptyList(),
+        emotionTags = this.emotionTags.map { ReadingRecord.EmotionTag.newInstance(it) },
        // ... existing fields ...
    )
}
📝 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
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
)
}
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(),
+ emotionTags = this.emotionTags.map { ReadingRecord.EmotionTag.newInstance(it) },
createdAt = this.createdAt,
updatedAt = this.updatedAt,
deletedAt = this.deletedAt
)
}
🤖 Prompt for AI Agents
In
infra/src/main/kotlin/org/yapp/infra/readingrecord/entity/ReadingRecordEntity.kt
around lines 45 to 57, the toDomain() method sets emotionTags to an empty list,
missing the actual data mapping. To fix this, define an emotionTags field in the
entity using @ElementCollection or a separate entity to map the emotion tags
properly, then update the toDomain() method to convert and pass the real
emotionTags data instead of an empty list.

import org.yapp.domain.readingrecord.ReadingRecord
import org.yapp.domain.readingrecord.ReadingRecordRepository
import org.yapp.domain.readingrecord.ReadingRecordSortType
import org.yapp.domain.userbook.UserBookSortType
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를 제거해주세요.

UserBookSortType이 import되어 있지만 실제로는 사용되지 않고 있습니다.

- import org.yapp.domain.userbook.UserBookSortType
📝 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 org.yapp.domain.userbook.UserBookSortType
🤖 Prompt for AI Agents
In
infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/ReadingRecordRepositoryImpl.kt
at line 10, the import statement for UserBookSortType is unused. Remove this
import statement to clean up the code and avoid unnecessary dependencies.

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
4.2% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 49104b6 and 413f117.

📒 Files selected for processing (27)
  • apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookCreateRequest.kt (2 hunks)
  • apis/src/main/kotlin/org/yapp/apis/book/dto/request/UpsertUserBookRequest.kt (3 hunks)
  • apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookCreateResponse.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/book/dto/response/UserBookPageResponse.kt (2 hunks)
  • domain/src/main/kotlin/org/yapp/domain/book/Book.kt (5 hunks)
  • domain/src/main/kotlin/org/yapp/domain/book/BookDomainService.kt (3 hunks)
  • domain/src/main/kotlin/org/yapp/domain/book/BookRepository.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/book/vo/BookInfoVO.kt (2 hunks)
  • domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecord.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/readingrecordtag/ReadingRecordTag.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/readingrecordtag/ReadingRecordTagRepository.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/tag/Tag.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/tag/TagRepository.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/userbook/UserBook.kt (6 hunks)
  • infra/src/main/kotlin/org/yapp/infra/book/entity/BookEntity.kt (3 hunks)
  • infra/src/main/kotlin/org/yapp/infra/book/repository/JpaBookRepository.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/infra/book/repository/impl/BookRepositoryImpl.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/infra/external/aladin/response/AladinResponseBase.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/infra/readingrecordtag/entity/ReadingRecordTagEntity.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/JpaReadingRecordTagRepository.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/ReadingRecordTagRepositoryImpl.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/infra/tag/entity/TagEntity.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/infra/tag/repository/JpaTagRepository.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/infra/tag/repository/TagRepositoryImpl.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/infra/userbook/entity/UserBookEntity.kt (5 hunks)
  • infra/src/main/kotlin/org/yapp/infra/userbook/repository/JpaUserBookRepository.kt (1 hunks)
🔇 Additional comments (43)
infra/src/main/kotlin/org/yapp/infra/external/aladin/response/AladinResponseBase.kt (1)

23-24: 새로운 itemPage 필드 추가가 적절합니다.

외부 API 응답에서 페이지네이션 정보를 포함하는 것은 합리적인 개선사항입니다. nullable Int?로 정의되어 기존 API 호환성도 유지됩니다.

apis/src/main/kotlin/org/yapp/apis/book/dto/response/BookCreateResponse.kt (2)

8-8: UUID 기반 bookId 추가가 적절합니다.

도메인 모델의 UUID 식별자 전환과 일치하며, 북 생성 응답에서 일관된 식별자를 제공합니다.


18-18: 팩토리 메서드에서 올바르게 UUID 값을 추출합니다.

bookVO.id.value를 통해 도메인 객체에서 UUID 값을 적절히 추출하고 있습니다.

infra/src/main/kotlin/org/yapp/infra/book/repository/JpaBookRepository.kt (2)

11-11: UUID 기반 JPA Repository로의 전환이 적절합니다.

기본 키 타입을 String에서 UUID로 변경하여 도메인 모델의 식별자 전략과 일치시켰습니다.


12-13: ISBN 기반 조회 메서드 추가가 필요한 개선사항입니다.

UUID가 기본 키가 된 후에도 ISBN으로 북을 조회할 수 있는 메서드들을 제공하여 기능적 호환성을 유지합니다. Spring Data JPA 명명 규칙을 올바르게 따르고 있습니다.

domain/src/main/kotlin/org/yapp/domain/book/vo/BookInfoVO.kt (2)

6-6: 도메인 식별자 추가가 적절합니다.

Book.Id 타입의 id 프로퍼티를 첫 번째 매개변수로 추가하여 도메인 모델의 UUID 식별자 전환을 올바르게 반영했습니다.


26-26: 팩토리 메서드에서 올바른 id 매핑을 수행합니다.

도메인 객체의 id를 value object에 적절히 매핑하고 있습니다.

infra/src/main/kotlin/org/yapp/infra/userbook/repository/JpaUserBookRepository.kt (1)

9-10: 사용자 북 조회를 위한 새로운 메서드들이 적절히 추가되었습니다.

findByBookIdAndUserIdfindByIdAndUserId 메서드는 새로운 bookId 관계와 사용자별 조회 기능을 지원합니다. Spring Data JPA 명명 규칙을 올바르게 따르고 있으며, nullable 반환 타입이 적절합니다.

apis/src/main/kotlin/org/yapp/apis/book/dto/response/UserBookPageResponse.kt (1)

19-22: 총 개수 필드 추가가 적절합니다.

totalCount 필드를 추가하여 전체 책 개수를 제공하는 것이 사용자 경험 측면에서 유용합니다. 계산 로직(beforeReadingCount + readingCount + completedCount)도 정확하고, 스키마 문서화도 적절히 되어 있습니다.

Also applies to: 31-31, 36-37

domain/src/main/kotlin/org/yapp/domain/book/BookDomainService.kt (2)

8-8: UUID 기반 식별자 전환이 적절합니다.

findById 메소드가 UUID를 매개변수로 사용하도록 변경된 것이 도메인 모델의 일관성을 높입니다. ISBN은 여전히 findByIsbnOrNull을 통해 조회 가능하므로 기능상 문제가 없습니다.

Also applies to: 14-16


44-44: 저장소 메소드 호출이 올바르게 수정되었습니다.

ISBN 기반 조회를 위해 findByIsbnexistsByIsbn을 사용하는 것이 논리적으로 정확합니다. 도메인 서비스의 역할에 맞게 적절히 구현되었습니다.

Also applies to: 58-58

domain/src/main/kotlin/org/yapp/domain/book/Book.kt (3)

3-3: UUID 기반 식별자 도입이 잘 구현되었습니다.

Book.Id 인라인 값 클래스를 사용하여 타입 안전성을 보장하면서 UUID 기반 식별자를 도입한 것이 좋은 설계입니다. UuidGenerator를 사용한 ID 생성도 적절합니다.

Also applies to: 6-6, 12-12


75-80: 인라인 값 클래스 구현이 적절합니다.

Id 인라인 값 클래스가 UUID를 래핑하여 타입 안전성을 제공하며, 팩토리 메소드 newInstance를 통해 일관된 객체 생성 패턴을 따르고 있습니다.


35-35: 팩토리 메소드들이 올바르게 업데이트되었습니다.

create 메소드에서 새로운 ID를 생성하고, reconstruct 메소드에서 기존 ID를 받아 처리하는 로직이 적절합니다. 도메인 객체의 생성과 재구성 패턴을 잘 따르고 있습니다.

Also applies to: 47-47, 60-60

domain/src/main/kotlin/org/yapp/domain/book/BookRepository.kt (1)

3-3: 저장소 인터페이스 업데이트가 적절합니다.

UUID 기반 기본 키 조회(findById, existsById)와 ISBN 기반 비즈니스 키 조회(findByIsbn, existsByIsbn)를 분리하여 제공하는 것이 좋은 설계입니다. 두 가지 접근 패턴을 모두 지원하여 유연성을 제공합니다.

Also applies to: 6-9

apis/src/main/kotlin/org/yapp/apis/book/dto/request/UpsertUserBookRequest.kt (1)

10-10: bookId 필드 추가가 일관성 있게 구현되었습니다.

새로운 bookId 필드와 validBookId() 검증 메소드가 기존 패턴을 잘 따르고 있으며, 팩토리 메소드에서 BookCreateResponse로부터 적절히 매핑되고 있습니다. UUID 기반 식별자 전환이 API 계층까지 일관되게 적용되었습니다.

Also applies to: 19-19, 35-35

infra/src/main/kotlin/org/yapp/infra/userbook/entity/UserBookEntity.kt (3)

15-16: 소프트 삭제 필터링이 적절히 구현되었습니다.

@SQLRestriction 어노테이션을 추가하여 삭제된 레코드가 자동으로 쿼리에서 제외되도록 한 것은 좋은 접근입니다.


27-29: bookId 필드가 적절히 추가되었습니다.

UUID 타입의 bookId 필드가 올바른 JPA 어노테이션과 함께 추가되었으며, 도메인 모델 변경사항과 일치합니다.


65-65: 매핑 메서드가 bookId를 포함하도록 올바르게 업데이트되었습니다.

toDomain()fromDomain() 메서드가 새로운 bookId 필드를 적절히 처리하도록 수정되었습니다.

Also applies to: 82-82

domain/src/main/kotlin/org/yapp/domain/userbook/UserBook.kt (3)

11-11: bookId 속성이 도메인 모델에 적절히 추가되었습니다.

기존 ID 패턴을 따라 BookId 값 클래스를 사용한 것이 일관성 있고 좋습니다.


29-29: 팩토리 메서드가 bookId를 포함하도록 올바르게 업데이트되었습니다.

create()reconstruct() 메서드가 새로운 bookId 매개변수를 적절히 처리합니다.

Also applies to: 40-40, 53-53, 67-67


95-100: BookId 값 클래스가 일관된 패턴으로 구현되었습니다.

기존의 Id, UserId 값 클래스와 동일한 패턴을 따라 구현되어 코드 일관성이 유지됩니다.

infra/src/main/kotlin/org/yapp/infra/book/repository/impl/BookRepositoryImpl.kt (2)

15-21: ID 타입이 UUID로 적절히 변경되었습니다.

Book 엔티티의 기본 식별자가 UUID로 변경됨에 따라 레포지토리 메서드들이 올바르게 업데이트되었습니다.


23-29: ISBN 기반 조회 메서드가 적절히 추가되었습니다.

UUID로의 전환 과정에서 ISBN을 통한 조회 기능을 유지한 것은 하위 호환성과 유연성 측면에서 좋은 결정입니다.

infra/src/main/kotlin/org/yapp/infra/book/entity/BookEntity.kt (3)

62-62: 매핑 메서드가 올바르게 업데이트되었습니다.

toDomain()fromDomain() 메서드가 새로운 id 필드를 적절히 처리합니다.

Also applies to: 77-77


91-91: equals/hashCode 구현이 적절히 업데이트되었습니다.

기본 키 변경에 맞춰 id 필드를 사용하도록 수정된 것이 올바릅니다.

Also applies to: 94-94


33-33: description 필드가 적절히 추가되었습니다.

nullable 필드로 추가되어 기존 데이터와의 호환성을 유지하면서 책 정보를 풍부하게 만들 수 있습니다.

Also applies to: 57-58, 84-84

infra/src/main/kotlin/org/yapp/infra/tag/repository/JpaTagRepository.kt (1)

7-11: 잘 설계된 JPA 레포지토리 인터페이스입니다.

Spring Data JPA 컨벤션을 잘 따르고 있으며, 태그 관련 필수 쿼리 메서드들이 적절히 정의되어 있습니다. 메서드 시그니처와 반환 타입이 올바르게 설정되어 있습니다.

infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/JpaReadingRecordTagRepository.kt (1)

7-9: 적절하게 설계된 JPA 레포지토리 인터페이스입니다.

독서기록과 태그 간의 다대다 관계를 지원하는 필수 쿼리 메서드가 올바르게 정의되어 있습니다.

infra/src/main/kotlin/org/yapp/infra/tag/entity/TagEntity.kt (4)

15-27: 잘 설계된 JPA 엔티티입니다.

소프트 삭제, 타임스탬프 관리, 도메인 변환 메서드가 모두 적절히 구현되어 있습니다.


29-37: 도메인 변환 메서드가 올바르게 구현되었습니다.

BaseTimeEntity의 타임스탬프 필드들이 적절히 매핑되어 있습니다.


39-46: 팩토리 메서드가 적절히 구현되었습니다.

도메인 객체에서 엔티티로의 변환이 올바르게 처리되어 있습니다.


48-55: equals와 hashCode 구현이 적절합니다.

UUID 기반의 동등성 비교가 올바르게 구현되어 있습니다.

domain/src/main/kotlin/org/yapp/domain/tag/TagRepository.kt (1)

5-10: 도메인 레포지토리 인터페이스가 잘 설계되었습니다.

단일 및 배치 연산을 모두 지원하며 도메인 주도 설계 원칙에 맞게 구현되어 있습니다. 메서드 시그니처가 일관성 있고 적절합니다.

domain/src/main/kotlin/org/yapp/domain/readingrecordtag/ReadingRecordTagRepository.kt (1)

5-8: 독서기록-태그 관계를 위한 레포지토리 인터페이스가 적절히 설계되었습니다.

배치 저장과 독서기록별 태그 조회를 지원하는 메서드들이 올바르게 정의되어 있어, 다대다 관계 관리에 효율적입니다.

infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/ReadingRecordTagRepositoryImpl.kt (1)

12-15: 데이터베이스 예외 처리 부재 확인 및 수동 검증 필요

다른 *RepositoryImpl.kt 파일들에서 try/catch 또는 throw 패턴이 전혀 감지되지 않았습니다.
서버 환경이나 라이브러리 예외 흐름에 따라 JPA 예외 처리 로직이 누락될 수 있으니, 실제 코드 베이스 전반을 검토하셔서 필요한 곳에 적절한 예외 처리 또는 로깅을 추가해 주세요.

domain/src/main/kotlin/org/yapp/domain/tag/Tag.kt (1)

7-13: 도메인 모델 설계가 잘 되어 있습니다.

private constructor와 companion object factory methods를 사용한 설계가 DDD 원칙에 잘 부합합니다. 도메인 객체의 생성을 통제하고 불변성을 보장하는 좋은 패턴입니다.

infra/src/main/kotlin/org/yapp/infra/readingrecordtag/entity/ReadingRecordTagEntity.kt (3)

15-32: JPA 엔티티 설계가 잘 되어 있습니다.

소프트 삭제를 위한 @SQLDelete@SQLRestriction 사용, UUID를 VARCHAR로 매핑하는 @JdbcTypeCode 설정 등이 적절히 구성되어 있습니다.


55-62: equals와 hashCode 구현이 적절합니다.

ID 기반의 equality 비교는 JPA 엔티티에서 권장되는 패턴입니다. 동일한 ID를 가진 엔티티는 동일한 것으로 간주되어야 합니다.


34-43: 도메인 매핑 검증 완료: BaseTimeEntity 타임스탬프 필드가 올바르게 매핑되었습니다
toDomain() 메서드에서 createdAt, updatedAt, deletedAt가 각각 BaseTimeEntity의 필드 값 그대로 정상적으로 전달되고 있어 추가 수정이 필요 없습니다.

infra/src/main/kotlin/org/yapp/infra/tag/repository/TagRepositoryImpl.kt (1)

9-28: Repository 구현이 일관된 패턴을 잘 따르고 있습니다.

도메인과 인프라 계층 간의 변환 로직이 명확하고, null 안전성도 적절히 처리되어 있습니다. 특히 findByName에서 nullable 반환을 올바르게 처리한 점이 좋습니다.

domain/src/main/kotlin/org/yapp/domain/readingrecordtag/ReadingRecordTag.kt (2)

18-24: 연관 엔티티 설계가 적절합니다.

원시 UUID를 받아서 도메인의 타입 안전한 ID로 변환하는 접근 방식이 좋습니다. 다대다 관계를 명시적인 연관 엔티티로 모델링한 것도 도메인 설계 측면에서 적절한 선택입니다.


9-16: 도메인 모델의 일관성이 좋습니다.

다른 도메인 엔티티들과 동일한 패턴(private constructor, 타임스탬프 필드, 타입 안전한 ID)을 따르고 있어 코드베이스의 일관성을 유지하고 있습니다.

Comment on lines 23 to 26
val coverImageUrl: String,

val description: String? = null,
val description: String? = null
) {
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

마지막 파라미터에도 트레일링 콤마를 남겨 스타일을 통일하세요.

description 뒤의 트레일링 콤마를 제거하면서, 같은 파일 내 from() 호출부(45~46행)에는 콤마가 남아 있어 일관성이 깨집니다. Kotlin 1.4+에서는 멀티라인 인자 목록에 트레일링 콤마를 두는 것이 흔한 패턴이므로 선언부와 호출부 모두 동일한 규칙을 적용하는 편이 유지보수에 유리합니다.

-    val description: String? = null
+    val description: String? = null,
📝 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
val coverImageUrl: String,
val description: String? = null,
val description: String? = null
) {
val coverImageUrl: String,
val description: String? = null,
) {
🤖 Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookCreateRequest.kt
around lines 23 to 26, add a trailing comma after the last parameter
'description' in the constructor to maintain consistent style with the rest of
the file, especially matching the trailing comma usage in the 'from()' call
around lines 45 to 46. This ensures uniformity in multiline parameter lists and
improves maintainability.

Comment on lines +45 to 46
description = bookDetail.description,
)
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

선언부와 호출부의 트레일링 콤마 스타일이 불일치합니다.

위 25행에서 콤마를 제거했다면, 여기서도 제거하여 일관성을 맞추거나 반대로 둘 다 유지하는 방향으로 통일해 주세요.
현재처럼 혼재되면 차후 자동 포매터(ktlint 등) 적용 시 불필요한 변경이 반복적으로 발생할 수 있습니다.

🤖 Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/book/dto/request/BookCreateRequest.kt
around lines 45 to 46, the trailing comma style between the declaration and call
sites is inconsistent. To fix this, either remove the trailing comma here to
match line 25 or add trailing commas in both places to maintain consistency and
prevent repeated formatting changes by tools like ktlint.

val pageNumber: PageNumber,
val quote: Quote,
val review: Review,
val emotionTags: List<EmotionTag> = emptyList(),
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

emotionTags 필드의 용도를 명확히 해주세요.

ReadingRecord 도메인 모델에는 emotionTags 필드가 있지만, 실제 영속성 계층에서는 ReadingRecordTag를 통해 별도로 관리되고 있습니다. 이는 도메인 모델과 영속성 모델의 불일치로 혼란을 야기할 수 있습니다.

다음 중 하나를 고려해보세요:

  1. emotionTags를 도메인 모델에서 제거하고 필요시 별도로 조회
  2. 현재 설계를 유지한다면, 이 필드가 영속화되지 않고 조회 시에만 채워지는 transient 필드임을 문서화
/**
 * @property emotionTags 이 필드는 영속화되지 않으며, 조회 시 ReadingRecordTag를 통해 로드됩니다.
 */
val emotionTags: List<EmotionTag> = emptyList(),

Also applies to: 26-26, 34-34, 44-44, 55-55

🤖 Prompt for AI Agents
In domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecord.kt at
lines 13, 26, 34, 44, and 55, the emotionTags field in the domain model does not
align with persistence where ReadingRecordTag manages these tags separately. To
fix this, either remove the emotionTags field from the domain model and fetch
tags separately when needed, or keep the field but clearly document it as
transient and not persisted, indicating it is populated only during retrieval,
for example by adding a KDoc comment explaining this behavior.

Comment on lines +52 to +64
fun findReadingRecordsByDynamicCondition(
userBookId: UUID,
sort: ReadingRecordSortType?,
pageable: Pageable
): Page<ReadingRecordInfoVO> {
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 })
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

N+1 쿼리 문제가 발생할 수 있습니다.

현재 구현에서는 각 ReadingRecord마다 개별적으로 태그를 조회하고 있어, 페이지당 레코드 수만큼 추가 쿼리가 발생합니다. 대량의 데이터 조회 시 성능 문제가 발생할 수 있습니다.

다음과 같이 벌크 조회로 최적화하는 것을 고려해보세요:

 fun findReadingRecordsByDynamicCondition(
     userBookId: UUID,
     sort: ReadingRecordSortType?,
     pageable: Pageable
 ): Page<ReadingRecordInfoVO> {
     val page = readingRecordRepository.findReadingRecordsByDynamicCondition(userBookId, sort, pageable)
+    
+    // 모든 reading record ID를 수집
+    val readingRecordIds = page.content.map { it.id.value }
+    
+    // 벌크로 모든 ReadingRecordTag 조회
+    val allReadingRecordTags = readingRecordTagRepository.findByReadingRecordIdIn(readingRecordIds)
+    val readingRecordTagMap = allReadingRecordTags.groupBy { it.readingRecordId.value }
+    
+    // 모든 tag ID 수집 및 벌크 조회
+    val allTagIds = allReadingRecordTags.map { it.tagId.value }.distinct()
+    val allTags = tagRepository.findByIds(allTagIds)
+    val tagMap = allTags.associateBy { it.id.value }
+    
     return page.map { readingRecord ->
-        val readingRecordTags = readingRecordTagRepository.findByReadingRecordId(readingRecord.id.value)
-        val tagIds = readingRecordTags.map { it.tagId.value }
-        val tags = tagRepository.findByIds(tagIds)
+        val readingRecordTags = readingRecordTagMap[readingRecord.id.value] ?: emptyList()
+        val tags = readingRecordTags.mapNotNull { tagMap[it.tagId.value] }
         ReadingRecordInfoVO.newInstance(readingRecord, tags.map { it.name })
     }
 }

필요한 Repository 메서드:

  • ReadingRecordTagRepository.findByReadingRecordIdIn(readingRecordIds: List<UUID>): List<ReadingRecordTag>

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 52 to 64, the current code fetches tags for each ReadingRecord
individually, causing an N+1 query problem. To fix this, first collect all
readingRecord IDs from the page, then use
ReadingRecordTagRepository.findByReadingRecordIdIn to fetch all related tags in
bulk. Map these tags by readingRecordId to associate them efficiently with each
ReadingRecord, and finally create ReadingRecordInfoVO instances using the
pre-fetched tags to avoid multiple queries.

Comment on lines +15 to +20
fun create(name: String): Tag {
return Tag(
id = Id.newInstance(UuidGenerator.create()),
name = name
)
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

태그 이름에 대한 유효성 검증을 추가하는 것을 고려해보세요.

태그 생성 시 이름이 비어있거나 공백만 있는 경우에 대한 검증이 없습니다. 도메인 규칙을 강화하면 데이터 품질을 향상시킬 수 있습니다.

 fun create(name: String): Tag {
+    require(name.isNotBlank()) { "태그 이름은 비어있을 수 없습니다" }
     return Tag(
         id = Id.newInstance(UuidGenerator.create()),
-        name = name
+        name = name.trim()
     )
 }
📝 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
fun create(name: String): Tag {
return Tag(
id = Id.newInstance(UuidGenerator.create()),
name = name
)
}
fun create(name: String): Tag {
require(name.isNotBlank()) { "태그 이름은 비어있을 수 없습니다" }
return Tag(
id = Id.newInstance(UuidGenerator.create()),
name = name.trim()
)
}
🤖 Prompt for AI Agents
In domain/src/main/kotlin/org/yapp/domain/tag/Tag.kt around lines 15 to 20, the
create function lacks validation for the tag name. Add a check to ensure the
name is not empty or only whitespace before creating the Tag instance. If the
validation fails, throw an appropriate exception to enforce domain rules and
improve data quality.

Comment on lines 17 to +18
@SQLDelete(sql = "UPDATE books SET deleted_at = NOW() WHERE isbn = ?")
@SQLRestriction("deleted_at IS NULL")
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

@SQLDelete 어노테이션의 WHERE 절이 잘못되었습니다.

기본 키가 id로 변경되었는데 @SQLDelete의 WHERE 절은 여전히 isbn을 사용하고 있습니다. 이는 소프트 삭제 시 오류를 발생시킬 수 있습니다.

다음과 같이 수정해야 합니다:

-@SQLDelete(sql = "UPDATE books SET deleted_at = NOW() WHERE isbn = ?")
+@SQLDelete(sql = "UPDATE books SET deleted_at = NOW() WHERE id = ?")
🤖 Prompt for AI Agents
In infra/src/main/kotlin/org/yapp/infra/book/entity/BookEntity.kt around lines
17 to 18, the @SQLDelete annotation's WHERE clause incorrectly uses 'isbn' as
the identifier, but the primary key has changed to 'id'. Update the WHERE clause
in @SQLDelete to use 'id' instead of 'isbn' to ensure correct soft deletion
behavior.

Comment on lines 20 to 26
@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,
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

기본 키가 UUID로 성공적으로 변경되었습니다.

ISBN에서 UUID로의 전환이 적절히 구현되었으며, ISBN은 고유 제약조건을 유지하여 데이터 무결성을 보장합니다.

이러한 변경은 시스템 전반에 영향을 미칠 수 있으므로, 기존 ISBN 기반 참조들이 모두 업데이트되었는지 확인이 필요합니다.

🤖 Prompt for AI Agents
In infra/src/main/kotlin/org/yapp/infra/book/entity/BookEntity.kt around lines
20 to 26, the primary key has been changed from ISBN to UUID correctly, and the
ISBN field retains its unique constraint. To complete this change, ensure that
all parts of the system referencing the book entity by ISBN are updated to use
the new UUID primary key instead. Review and modify any database queries,
service methods, and API endpoints that previously relied on ISBN as the
identifier to now use UUID to maintain consistency and avoid errors.

return jpaReadingRecordTagRepository.saveAll(entities).map { it.toDomain() }
}

override fun findByReadingRecordId(readingRecordId: java.util.UUID): List<ReadingRecordTag> {
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.UUID를 직접 사용하는 대신 파일 상단에 import 문을 추가하는 것이 좋습니다.

+import java.util.UUID
+
 @Repository
 class ReadingRecordTagRepositoryImpl(
     private val jpaReadingRecordTagRepository: JpaReadingRecordTagRepository
 ) : ReadingRecordTagRepository {
     override fun saveAll(readingRecordTags: List<ReadingRecordTag>): List<ReadingRecordTag> {
         val entities = readingRecordTags.map { ReadingRecordTagEntity.fromDomain(it) }
         return jpaReadingRecordTagRepository.saveAll(entities).map { it.toDomain() }
     }

-    override fun findByReadingRecordId(readingRecordId: java.util.UUID): List<ReadingRecordTag> {
+    override fun findByReadingRecordId(readingRecordId: UUID): List<ReadingRecordTag> {
         return jpaReadingRecordTagRepository.findByReadingRecordId(readingRecordId).map { it.toDomain() }
     }
🤖 Prompt for AI Agents
In
infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/ReadingRecordTagRepositoryImpl.kt
at line 17, the usage of java.util.UUID should be replaced by adding an import
statement for java.util.UUID at the top of the file. Remove the fully qualified
name from the method signature and use the imported UUID type instead.

Comment on lines +25 to +26
@Column(nullable = false, length = 10, unique = true)
val name: String
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

태그 이름 길이 제한 검토를 권장합니다.

태그 이름을 10자로 제한하는 것이 실제 사용 시나리오에 적합한지 검토해보시기 바랍니다. 일부 의미 있는 태그명이 10자를 초과할 수 있습니다.

🤖 Prompt for AI Agents
In infra/src/main/kotlin/org/yapp/infra/tag/entity/TagEntity.kt around lines 25
to 26, the tag name length is limited to 10 characters, which may be too
restrictive for real use cases. Review the typical tag names expected in the
application and increase the length limit in the @Column annotation if necessary
to accommodate longer meaningful tag names.

@move-hoon move-hoon changed the title Book 163 feature/#52 feat: 독서기록 API 기능 구현 Jul 26, 2025
Copy link
Member

@move-hoon move-hoon left a comment

Choose a reason for hiding this comment

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

LGTM~~~

@minwoo1999 minwoo1999 merged commit 82f0a61 into develop Jul 26, 2025
4 of 5 checks passed
@move-hoon move-hoon deleted the BOOK-163-feature/#52 branch August 12, 2025 06:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BOOK-163/feat] 독서기록을 위한 API 기능개발을 진행합니다.

3 participants