-
Notifications
You must be signed in to change notification settings - Fork 1
feat: 독서기록 API 기능 구현 #53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
📝 Walkthrough""" Walkthrough이번 변경 사항은 독서기록(ReadingRecord) 도메인에 대한 전체적인 API, 서비스, 도메인, 인프라 계층의 신규 도입과 함께, UserBook 관련 정렬 방식의 enum 도입 및 적용, UserBook 조회 및 검증 로직 강화, 예외 및 에러코드 신설, 일부 DTO 생성자 접근 제어 강화 등이 포함됩니다. 또한, 도메인-인프라 계층 간 매핑 및 쿼리 메서드가 대거 추가되었습니다. Changes
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
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>
Estimated code review effort🎯 4 (Complex) | ⏱️ ~40–60 minutes
Possibly related PRs
Suggested reviewers
Note ⚡️ Unit Test Generation is now available in beta!Learn more here, or try it out under "Finishing Touches" below. ✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed 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)
Other keywords and placeholders
Documentation and Community
|
There was a problem hiding this 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
📒 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가 적절합니다.
UserBookErrorCode와UserBookNotFoundExceptionimport로 표준화된 오류 처리가 가능해졌습니다.
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 문서화가 포함되어 있습니다.
| 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 | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
팩토리 메서드 구현이 잘 되었지만 nullability 일관성을 개선할 수 있습니다.
팩토리 메서드 of를 통한 객체 생성 제어는 잘 구현되었습니다. 하지만 다음과 같은 개선점이 있습니다:
- 팩토리 메서드에서는
query를 non-null로 받지만, 생성자에서는 nullable로 정의되어 있어 일관성이 부족합니다. 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.
apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerApi.kt
Outdated
Show resolved
Hide resolved
| fun validPageNumber(): Int = pageNumber!! | ||
| fun validQuote(): String = quote!! | ||
| fun validReview(): String = review!! | ||
| fun validEmotionTags(): List<String> = emotionTags!! |
There was a problem hiding this comment.
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.
| 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 | ||
| ) | ||
| } |
There was a problem hiding this comment.
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.
| 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.
domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt
Outdated
Show resolved
Hide resolved
domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordRepository.kt
Show resolved
Hide resolved
domain/src/main/kotlin/org/yapp/domain/readingrecord/vo/ReadingRecordInfoVO.kt
Outdated
Show resolved
Hide resolved
| 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 |
There was a problem hiding this comment.
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.
...otlin/org/yapp/infra/readingrecord/repository/impl/JpaReadingRecordQuerydslRepositoryImpl.kt
Outdated
Show resolved
Hide resolved
...rc/main/kotlin/org/yapp/infra/readingrecord/repository/JpaReadingRecordQuerydslRepository.kt
Show resolved
Hide resolved
move-hoon
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
고생하셨습니다 민우님!!
몇가지 코멘트 남겼습니다. 정말정말 감사합니다
| 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 | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
해당 정적 팩토리 메서드는 사용중이지 않은데 만들게 되신 이유가 궁금합니다!
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
여기에 RequestMapping 추가해주시면 감사하겠습니다! (가독성을 위해서)
| 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) | ||
| } |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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}")
이런식으로 진행하면 좋을 거 같아요!
| companion object { | ||
| fun of( | ||
| pageNumber: Int, | ||
| quote: String, | ||
| review: String, | ||
| emotionTags: List<String> | ||
| ): CreateReadingRecordRequest { | ||
| return CreateReadingRecordRequest( | ||
| pageNumber = pageNumber, | ||
| quote = quote, | ||
| review = review, | ||
| emotionTags = emotionTags | ||
| ) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
여기서도 정팩메가 안쓰여서 삭제해도 될 것 같다는 생각이 듭니다!!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
네네 여기도 정팩메 안쓰니까 삭제할게요!
domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt
Outdated
Show resolved
Hide resolved
domain/src/main/kotlin/org/yapp/domain/readingrecord/vo/ReadingRecordInfoVO.kt
Outdated
Show resolved
Hide resolved
| @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() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ElementCollection도 외래 키를 생성하는 것으로 알고 있습니다.
하지만 값 타입 컬렉션을 저장하기에 적합한 기능이기 때문에, 별도의 테이블을 정의하고 식별자를 부여하는 방식으로 구현할지, 아니면 @ElementCollection을 사용하여 간단하게 매핑할지 고민이 됩니다!!
이 부분에 대해서 민우님의 생각이 궁금합니다 ㅎㅎ
...rc/main/kotlin/org/yapp/infra/readingrecord/repository/JpaReadingRecordQuerydslRepository.kt
Show resolved
Hide resolved
...otlin/org/yapp/infra/readingrecord/repository/impl/JpaReadingRecordQuerydslRepositoryImpl.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this 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
📒 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가 적절합니다.
UserBookErrorCode와UserBookNotFoundExceptionimport가 새로운 검증 기능에 필요한 예외 처리를 위해 적절히 추가되었습니다.
| 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" | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
예외 메시지에서 민감한 정보 노출을 고려해주세요.
메서드 구현은 적절하지만 다음 사항들을 검토해보시기 바랍니다:
- 보안 고려사항: 예외 메시지에
userBookId와userId가 직접 노출되고 있습니다. 로그나 클라이언트 응답에서 이러한 내부 ID가 노출될 수 있습니다. - 파라미터 순서: 도메인 서비스의
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.
| 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.
There was a problem hiding this 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: 타입 안전성이 개선된 좋은 구현입니다.이전 리뷰에서 제안된
ReadingRecordSortTypeenum을 사용하여 정렬 파라미터의 타입 안전성을 확보했습니다. 런타임에 잘못된 정렬 기준이 전달될 위험이 제거되었습니다.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의
Page와Pageable을 직접 사용하고 있습니다. 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 setapis/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: 예외 메시지에서 민감한 정보 노출을 방지하고 매개변수 순서를 통일해주세요.예외 메시지에
userBookId와userId가 직접 노출되고 있으며, 도메인 서비스와 매개변수 순서가 일치하지 않습니다.다음과 같이 수정을 제안합니다:
- 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
📒 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: 도메인 특화 타입 사용으로 타입 안전성 향상.
UserBookSortTypeenum 도입으로 정렬 파라미터의 타입 안전성이 개선되었습니다.
67-67: 코드 스타일 개선.trailing comma 추가로 향후 diff 가독성이 향상됩니다.
77-77: 일관된 타입 안전성 적용.API 계층 전반에 걸쳐
UserBookSortTypeenum이 일관되게 적용되어 타입 안전성이 확보되었습니다.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 도입이 좋습니다.
UserBookSortTypeenum을 도입하여 문자열 기반 정렬에서 타입 안전한 정렬로 개선한 점이 훌륭합니다.
27-27: enum 기반 정렬 로직이 올바르게 구현되었습니다.
UserBookSortTypeenum을 사용하여 정렬 로직을 구현한 부분이 적절합니다. 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: 사용자 검증을 위한 새로운 쿼리 메서드가 잘 추가되었습니다.
findByBookIdAndUserId와findByIdAndUserId메서드 추가로 사용자별 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 추가가 적절합니다.
UserBookSortTypeimport 추가로 문자열 기반에서 enum 기반 정렬로 개선되었습니다.
23-29: 새로운 쿼리 메서드들이 올바르게 구현되었습니다.
findByBookIdAndUserId와findByIdAndUserId메서드가 기존 패턴과 일관성 있게 구현되었습니다. JPA repository 위임과 도메인 객체 변환 로직이 적절합니다.
51-51: 정렬 파라미터 타입 변경이 일관성 있게 적용되었습니다.
UserBookSortType?으로 타입 변경이 도메인 레이어와 일관성 있게 적용되어 타입 안전성이 향상되었습니다.infra/src/main/kotlin/org/yapp/infra/readingrecord/repository/impl/JpaReadingRecordQuerydslRepositoryImpl.kt (1)
16-57: 구현이 잘 되어 있습니다.이전 리뷰 의견이 반영되어
whereCondition을 추출하여 중복을 제거했고, 타입 안전한 정렬을 위해ReadingRecordSortTypeenum을 사용하고 있습니다.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 규약을 올바르게 따르고 있습니다.
| @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 |
There was a problem hiding this comment.
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.
| @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.
| 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 | ||
| ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
사용하지 않는 의존성을 제거해주세요.
BookQueryService와 BookDomainService가 주입되고 있지만 실제로는 사용되지 않고 있습니다. 불필요한 결합도를 줄이기 위해 제거해주세요.
다음과 같이 수정을 제안합니다:
@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.
| 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, |
There was a problem hiding this comment.
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"
doneLength 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 -C3Length 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, |
There was a problem hiding this comment.
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.
| 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 | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
emotionTags 구현이 누락되었습니다.
toDomain() 메서드에서 emotionTags를 emptyList()로 하드코딩하고 있습니다. 엔티티에 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.
| 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 |
There was a problem hiding this comment.
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.
| 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.
|
There was a problem hiding this 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
📒 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: 사용자 북 조회를 위한 새로운 메서드들이 적절히 추가되었습니다.
findByBookIdAndUserId와findByIdAndUserId메서드는 새로운 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 기반 조회를 위해
findByIsbn과existsByIsbn을 사용하는 것이 논리적으로 정확합니다. 도메인 서비스의 역할에 맞게 적절히 구현되었습니다.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)을 따르고 있어 코드베이스의 일관성을 유지하고 있습니다.
| val coverImageUrl: String, | ||
|
|
||
| val description: String? = null, | ||
| val description: String? = null | ||
| ) { |
There was a problem hiding this comment.
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.
| 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.
| description = bookDetail.description, | ||
| ) |
There was a problem hiding this comment.
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(), |
There was a problem hiding this comment.
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를 통해 별도로 관리되고 있습니다. 이는 도메인 모델과 영속성 모델의 불일치로 혼란을 야기할 수 있습니다.
다음 중 하나를 고려해보세요:
emotionTags를 도메인 모델에서 제거하고 필요시 별도로 조회- 현재 설계를 유지한다면, 이 필드가 영속화되지 않고 조회 시에만 채워지는 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.
| 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 }) | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| fun create(name: String): Tag { | ||
| return Tag( | ||
| id = Id.newInstance(UuidGenerator.create()), | ||
| name = name | ||
| ) | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| @SQLDelete(sql = "UPDATE books SET deleted_at = NOW() WHERE isbn = ?") | ||
| @SQLRestriction("deleted_at IS NULL") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
| @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, |
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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.
| @Column(nullable = false, length = 10, unique = true) | ||
| val name: String |
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM~~~


🔗 관련 이슈
Close [BOOK-163/feat] 독서기록을 위한 API 기능개발을 진행합니다. #52
📘 작업 유형
✨ 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 계층:
ReadingRecordControllerApi및ReadingRecordController: 독서 기록 생성 및 페이징 처리된 목록 조회를 위한 REST API 엔드포인트를 구현했습니다.CreateReadingRecordRequestDTO: 독서 기록 생성을 위한 요청 객체로, 유효성 검증(@Min,@Max,@NotBlank,@Size)을 포함합니다.ReadingRecordResponseDTO: 독서 기록 조회 결과를 반환하는 응답 객체를 정의했습니다.애플리케이션 서비스 계층:
ReadingRecordService: 독서 기록 생성 및 동적 조건 조회를 위한 애플리케이션 서비스를 구현했습니다.ReadingRecordUseCase:UserAuthService와UserBookService를 활용하여 사용자 및 사용자 책의 유효성을 검증하고, 독서 기록의 생성 및 조회 흐름을 조정하는 유스케이스를 구현했습니다.📚 사용자 책(User Book) 관련 개선
UserBookService리팩토링:upsertUserBook메서드의 파라미터 전달 방식을 변경하고 반환 값을UserBookInfoVO로 통일했습니다.특정
userBookId와userId에 해당하는UserBook의 존재 여부를 검증하고 반환하는validateUserBookExists메서드를 추가했습니다.findAllUserBooks(userId: UUID)메서드는 제거하고validateUserBookExists로 대체했습니다.예외 처리 강화:
UserBookErrorCode와UserBookNotFoundException을 추가하여 사용자 책을 찾을 수 없을 때의 명확한 예외 처리를 구현했습니다.BookSearchRequest변경:companion object내부에of팩토리 메서드를 추가하여 객체 생성을 위한 명시적인 방법을 제공합니다.DTO 생성자 접근 제어자 변경:
DeleteTokenRequest,FindUserIdentityRequest,TokenGenerateRequest,UserBooksByIsbnsRequest의 주 생성자를private constructor로 변경하여 객체 생성 방식의 통일성을 높였습니다.💾 인프라 계층
ReadingRecordEntity: 독서 기록 도메인 모델의 영속성 엔티티를 정의하고,BaseTimeEntity를 상속받아 생성 및 수정 시간을 자동으로 관리하도록 했습니다.@SQLDelete를 사용하여 소프트 딜리트(Soft Delete)를 구현했습니다.Repository 구현:
JpaReadingRecordRepository,JpaReadingRecordQuerydslRepository,ReadingRecordRepositoryImpl을 통해 독서 기록의 CRUD 및 동적 쿼리 기능을 지원합니다.UserBookRepository변경:findByIdAndUserId메서드를 추가하여 사용자 ID 기반의 사용자 책 조회를 지원합니다.🧪 테스트 내역
브라우저/기기에서 동작 확인
엣지 케이스 테스트 완료
기존 기능 영향 없음
🎨 스크린샷 또는 시연 영상 (선택)
✅ PR 체크리스트
커밋 메시지가 명확합니다
PR 제목이 컨벤션에 맞습니다
관련 이슈 번호를 작성했습니다
기능이 정상적으로 작동합니다
불필요한 코드를 제거했습니다
💬 추가 설명 or 리뷰 포인트 (선택)
독서 기록 생성 및 조회 로직: 사용자 및 사용자 책의 존재 여부를 검증하는 로직과 페이지네이션 및 정렬을 포함한 조회 로직의 적절성을 확인해주세요.
새로운 예외 처리:
UserBookNotFoundException도입이 전반적인 예외 처리 전략과 잘 부합하는지 검토해주세요.Summary by CodeRabbit
신규 기능
기능 개선
버그 수정
문서화