Skip to content

Conversation

@move-hoon
Copy link
Member

@move-hoon move-hoon commented Dec 24, 2025

🔗 관련 이슈

📘 작업 유형

  • ✨ Feature (기능 추가)
  • 🐞 Bugfix (버그 수정)
  • 🔧 Refactor (코드 리팩토링)
  • ⚙️ Chore (환경 설정)
  • 📝 Docs (문서 작성 및 수정)
  • ✅ Test (기능 테스트)
  • 🎨 style (코드 스타일 수정)

📙 작업 내역

감정 시스템 V2 구조

  • 대분류 감정 5개: 따뜻함, 즐거움, 슬픔, 깨달음, 기타
  • 세부 감정 태그: 대분류별 여러 세부 감정 지원 (다중 선택 가능)

Domain Layer

  • PrimaryEmotion enum 추가 (대분류 감정)
  • DetailTag 도메인 및 Repository 추가 (세부 감정)
  • ReadingRecordDetailTag 도메인 및 Repository 추가 (독서기록-세부감정 연결)
  • DetailTagDomainService, ReadingRecordDetailTagDomainService 추가
  • ReadingRecord에 primaryEmotion 필드 추가, pageNumber nullable 변경

Infra Layer

  • DB 마이그레이션 스크립트 추가 (V20251224_001)
    • detail_tags 테이블 생성
    • reading_record_detail_tags 테이블 생성
    • reading_records.primary_emotion 컬럼 추가
  • DetailTagEntity, ReadingRecordDetailTagEntity 추가
  • Repository 구현체 추가

APIs Layer (V2)

  • POST /api/v2/reading-records/{userBookId} - 독서 기록 생성
  • GET /api/v2/reading-records/detail/{readingRecordId} - 독서 기록 상세 조회
  • GET /api/v2/reading-records/{userBookId} - 독서 기록 목록 조회
  • PUT /api/v2/reading-records/{readingRecordId} - 독서 기록 수정
  • DELETE /api/v2/reading-records/{readingRecordId} - 독서 기록 삭제
  • GET /api/v2/emotions - 감정 목록 조회 API

DTO 리팩토링

  • V2 DTO에 private constructor + 정적 팩토리 메서드 패턴 일관성 있게 적용
  • PrimaryEmotionDto, DetailEmotionDto, EmotionGroupDto, EmotionDetailDto를 nested class로 리팩토링
  • CreateReadingRecordRequestV2에 @NotNull validation 추가

primaryEmotion 변경 시 데이터 일관성 보장

  • 대분류 감정 변경 시 세부 감정 태그 처리 로직 추가
  • 대분류 변경 + 새 태그 제공 → 새 태그로 교체
  • 대분류 변경 + 태그 미제공 → 기존 태그 삭제 (불일치 방지)
  • 대분류 유지 + 새 태그 제공 → 새 태그로 교체
  • 대분류 유지 + 태그 미제공 → 기존 태그 유지

소유권 검증 강화 (V1/V2 공통)

  • getReadingRecordDetail, updateReadingRecord, deleteReadingRecord에 소유권 검증 추가
  • USER_BOOK_ACCESS_DENIED (403 FORBIDDEN) 에러 코드 추가
  • 다른 사용자의 독서 기록에 무단 접근 시 403 반환

API 문서 개선

  • 정렬 파라미터 우선순위 명확화 (sort 파라미터 vs Pageable.sort)
  • pageNumber 필드에 "(선택)" 표시 추가

V1 호환성

  • 기존 V1 API는 그대로 유지
  • V1 Response의 pageNumber nullable 변경
  • V1에도 동일한 소유권 검증 적용

🧪 테스트 내역

  • 브라우저/기기에서 동작 확인
  • 엣지 케이스 테스트 완료
  • 기존 기능 영향 없음

✅ PR 체크리스트

  • 커밋 메시지가 명확합니다
  • PR 제목이 컨벤션에 맞습니다
  • 관련 이슈 번호를 작성했습니다
  • 기능이 정상적으로 작동합니다
  • 불필요한 코드를 제거했습니다

💬 추가 설명 or 리뷰 포인트

  • 커밋 구조: 17개 커밋이 구현 논리 순서대로 작성되어 있어 코드 리뷰 시 흐름 파악이 용이합니다
  • DomainService 분리: V2에서는 ApplicationService가 여러 DomainService를 조합하는 구조로 설계했습니다
  • TODO: ReadingRecordDomainService의 V1 관련 의존성은 추후 분리 예정 (주석 표시됨)
  • 세부 감정 데이터: 마이그레이션에서 초기 세부 감정 데이터가 INSERT됩니다
  • 보안 강화: V1/V2 모두 독서 기록 접근 시 소유권 검증 추가 (403 FORBIDDEN)

Summary by CodeRabbit

  • New Features

    • 감정 목록 조회 API 추가
    • 독서기록 V2 API 도입(주감정·세부감정 태그 지원, CRUD, 페이징/정렬)
  • Enhancements

    • 요청/응답 포맷 확장: 주감정, 세부감정 포함 및 pageNumber 선택형 처리
    • 세부감정(DetailTag) 도입 및 연결 테이블·관리 로직 추가
    • 마이그레이션으로 기존 데이터에 주감정 칼럼 및 초기 세부감정 데이터 추가
  • Bug Fixes / Changes

    • 사용자 책 권한 오류 코드 및 메시지 변경 (접근 거부)

✏️ Tip: You can customize this high-level summary in your review settings.

@move-hoon move-hoon requested a review from minwoo1999 December 24, 2025 10:09
@move-hoon move-hoon self-assigned this Dec 24, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 24, 2025

Walkthrough

상세 감정 태그 도메인·JPA·리포지토리, 감정 목록 API(v2), 독서기록 V2(생성/조회/수정/삭제) 서비스·유스케이스 및 DB 마이그레이션을 추가해 읽기 기록과 상세 감정 태그 연관을 지원합니다.

Changes

Cohort / File(s) 변경 요약
감정 API
apis/src/main/kotlin/org/yapp/apis/emotion/controller/EmotionController.kt, apis/src/main/kotlin/org/yapp/apis/emotion/controller/EmotionControllerApi.kt
GET /api/v2/emotions 엔드포인트 추가(컨트롤러 구현 + OpenAPI 인터페이스).
감정 DTO / 서비스
apis/src/main/kotlin/org/yapp/apis/emotion/dto/response/EmotionListResponse.kt, apis/src/main/kotlin/org/yapp/apis/emotion/service/EmotionService.kt
EmotionListResponse DTO와 EmotionService 추가, DetailTagDomainService로부터 목록 생성.
독서기록 V2 컨트롤러·API
apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerApiV2.kt, apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerV2.kt
V2 REST 인터페이스 및 컨트롤러(POST/GET(detail,list)/PUT/DELETE) 추가.
독서기록 V2 DTOs
apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequestV2.kt, apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/UpdateReadingRecordRequestV2.kt, apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/ReadingRecordResponseV2.kt
V2 요청/응답 DTO 추가(주감정, 상세 감정 태그 ID, 검증 헬퍼 포함).
서비스 / 유스케이스 V2
apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordServiceV2.kt, apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCaseV2.kt
CRUD, 상세 태그 검증/연결, 트랜잭션 경계가 있는 서비스·유스케이스 추가.
V1 DTO·서비스 조정
apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequest.kt, apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordService.kt, apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/ReadingRecordResponse.kt
V1의 helper 제거·호출부 수정, ReadingRecordResponse.pageNumber nullable로 변경.
도메인: 상세태그
domain/src/main/kotlin/org/yapp/domain/detailtag/DetailTag.kt, .../DetailTagRepository.kt, .../DetailTagDomainService.kt
DetailTag 도메인 엔티티·리포지토리 인터페이스·도메인 서비스 추가.
도메인: ReadingRecord 확장
domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecord.kt, .../PrimaryEmotion.kt, .../vo/ReadingRecordInfoVO.kt, .../ReadingRecordDomainService.kt
ReadingRecordprimaryEmotion·deletedAt 추가, pageNumber nullable화, PrimaryEmotion enum 및 VO 확장, 도메인 서비스에 V2 메서드 추가 및 V1 경로 유지.
도메인: ReadingRecordDetailTag
domain/src/main/kotlin/org/yapp/domain/readingrecorddetailtag/ReadingRecordDetailTag.kt, .../ReadingRecordDetailTagRepository.kt, .../ReadingRecordDetailTagDomainService.kt
ReadingRecord↔DetailTag 연관 모델·리포지토리·도메인 서비스 추가(생성/조회/삭제).
인프라: DetailTag JPA 및 리포지토리
infra/src/main/kotlin/org/yapp/infra/detailtag/entity/DetailTagEntity.kt, .../repository/DetailTagRepositoryImpl.kt, .../repository/JpaDetailTagRepository.kt
detail_tags JPA 엔티티와 리포지토리 구현 및 JPA 인터페이스 추가.
인프라: ReadingRecordDetailTag JPA 및 리포지토리
infra/src/main/kotlin/org/yapp/infra/readingrecorddetailtag/entity/ReadingRecordDetailTagEntity.kt, .../repository/ReadingRecordDetailTagRepositoryImpl.kt, .../repository/JpaReadingRecordDetailTagRepository.kt
reading_record_detail_tags 엔티티(soft-delete 포함) 및 리포지토리/구현 추가.
인프라: ReadingRecordEntity 변경
infra/src/main/kotlin/org/yapp/infra/readingrecord/entity/ReadingRecordEntity.kt
pageNumber nullable로 변경, primaryEmotion 컬럼·매핑 추가, toDomain/fromDomain 갱신.
DB 마이그레이션
infra/src/main/resources/db/migration/mysql/V20251224_001__add_primary_emotion_and_detail_tags.sql
reading_records.page_number NULL 허용, primary_emotion 추가·데이터 마이그레이션, detail_tags·reading_record_detail_tags 테이블 생성 및 초기 데이터 삽입.
에러 코드 변경·검증 경로
apis/src/main/kotlin/org/yapp/apis/book/exception/UserBookErrorCode.kt, .../service/UserBookService.kt, .../usecase/ReadingRecordUseCase.kt
USER_BOOK_NOT_FOUNDUSER_BOOK_ACCESS_DENIED로 교체 및 userBook 소유권 검증 경로에서 사용.
인프라 리포지토리 구현(ReadingRecordDetailTag)
infra/.../readingrecorddetailtag/repository/ReadingRecordDetailTagRepositoryImpl.kt
JPA 위임과 도메인↔엔티티 변환을 포함한 저장/조회/삭제 구현.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant EmotionController as EmotionController
    participant EmotionService as EmotionService
    participant DetailTagDomainService as DetailTagDomainService
    participant DetailTagRepository as DetailTagRepository
    participant Database as DB

    Client->>EmotionController: GET /api/v2/emotions
    EmotionController->>EmotionService: getEmotionList()
    EmotionService->>DetailTagDomainService: findAll()
    DetailTagDomainService->>DetailTagRepository: findAll()
    DetailTagRepository->>DB: SELECT detail_tags
    DB-->>DetailTagRepository: rows
    DetailTagRepository-->>DetailTagDomainService: List<DetailTag>
    DetailTagDomainService-->>EmotionService: List<DetailTag>
    Note right of EmotionService: PrimaryEmotion별 그룹화 후 DTO 생성
    EmotionService-->>EmotionController: EmotionListResponse
    EmotionController-->>Client: 200 OK (EmotionListResponse)
Loading
sequenceDiagram
    actor Client
    participant ReadingRecordControllerV2 as ControllerV2
    participant ReadingRecordUseCaseV2 as UseCaseV2
    participant UserService as UserSvc
    participant UserBookService as UserBookSvc
    participant ReadingRecordServiceV2 as ServiceV2
    participant DetailTagDomainService as DetailTagDS
    participant ReadingRecordDetailTagDomainService as RRDetailTagDS
    participant ReadingRecordDomainService as RRDomain
    participant Database as DB

    Client->>ControllerV2: POST /api/v2/reading-records/{userBookId}
    ControllerV2->>UseCaseV2: createReadingRecord(userId,userBookId,req)
    UseCaseV2->>UserSvc: findById(userId)
    UserSvc-->>UseCaseV2: User
    UseCaseV2->>UserBookSvc: findById(userBookId)
    UserBookSvc-->>UseCaseV2: UserBook
    UseCaseV2->>ServiceV2: createReadingRecord(userId,userBookId,req)
    ServiceV2->>DetailTagDS: findAllById(detailTagIds)
    DetailTagDS-->>ServiceV2: List<DetailTag>
    ServiceV2->>RRDomain: createReadingRecordV2(...)
    RRDomain->>DB: INSERT reading_records
    DB-->>RRDomain: persisted ReadingRecord
    RRDomain-->>ServiceV2: ReadingRecord
    ServiceV2->>RRDetailTagDS: createAndSaveAll(readingRecordId, detailTagIds)
    RRDetailTagDS->>DB: INSERT reading_record_detail_tags
    DB-->>RRDetailTagDS: persisted rows
    RRDetailTagDS-->>ServiceV2: List<ReadingRecordDetailTag>
    ServiceV2-->>UseCaseV2: ReadingRecordResponseV2
    UseCaseV2-->>ControllerV2: ReadingRecordResponseV2
    ControllerV2-->>Client: 201 Created (ReadingRecordResponseV2)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

🔨 refactor

Suggested reviewers

  • minwoo1999
  • hoonyworld

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Pull request title clearly describes the main changes: implementing Reading Record V2 API and primary/detail emotion system.
Linked Issues check ✅ Passed Code changes comprehensively implement the detailed emotion recording feature requested in #145: PrimaryEmotion enum, DetailTag/DetailTagDomainService domain layer, ReadingRecordDetailTag association, V2 API endpoints, and supporting DTOs.
Out of Scope Changes check ✅ Passed Changes to UserBookErrorCode (USER_BOOK_NOT_FOUND → USER_BOOK_ACCESS_DENIED) and ownership validation appear to be necessary security enhancements aligned with V2 API authorization, though not explicitly detailed in the linked issue.
✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ed4a5b7 and 4a5e4ec.

📒 Files selected for processing (7)
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerApiV2.kt
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerV2.kt
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordService.kt
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordServiceV2.kt
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordTagService.kt
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCase.kt
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCaseV2.kt
⏰ 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 (19)
apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordTagService.kt (1)

3-3: LGTM! 읽기 전용 트랜잭션 경계가 적절하게 적용되었습니다.

서비스 레이어에서 @Transactional(readOnly = true)를 사용하여 트랜잭션 경계를 정의하는 것은 Spring Framework의 모범 사례입니다. 읽기 전용 트랜잭션은 성능 최적화와 데이터 무결성 보장에 도움이 되며, PR 전체의 트랜잭션 표준화 패턴과도 일치합니다.

Also applies to: 14-14

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

20-38: 트랜잭션 경계 설정이 적절합니다.

createReadingRecord@Transactional을 추가하여 독서 기록 생성과 lastActivity 업데이트가 동일한 트랜잭션 내에서 원자적으로 처리됩니다.


31-31: emotionTags 검증 로직 이동 확인이 필요합니다.

request.validEmotionTags()에서 request.emotionTags로 변경되었습니다. 기존 validEmotionTags() 메서드가 수행하던 검증 또는 변환 로직이 다른 곳(DTO 어노테이션, 도메인 레이어 등)에서 처리되는지 확인해 주세요.


40-57: 읽기 전용 트랜잭션 설정이 적절합니다.

조회 메서드에 @Transactional(readOnly = true)를 적용하여 Hibernate flush 모드 최적화 및 DB 레플리카 라우팅이 가능합니다.

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

63-68: LGTM!

Page 결과를 ReadingRecordPageResponse로 변환하는 로직이 깔끔하게 구현되었습니다.

apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordServiceV2.kt (2)

27-58: LGTM! 트랜잭션 경계가 올바르게 설정되었습니다.

독서 기록 생성, 세부 감정 태그 저장, 사용자 마지막 활동 시간 업데이트가 모두 하나의 트랜잭션으로 묶여 원자성이 보장됩니다. 이전 리뷰에서 지적된 트랜잭션 이슈가 올바르게 해결되었습니다.


170-195: primaryEmotion 변경 시 detail tags 처리 로직이 올바릅니다.

이전 리뷰에서 제기된 primaryEmotion 변경 시 기존 detail emotion tags가 불일치하는 문제가 완벽하게 해결되었습니다:

  1. 새로운 detail tag IDs가 제공되면 기존 태그를 삭제하고 새로 생성
  2. primaryEmotion만 변경되고 detail tag IDs가 없으면 기존 태그를 자동 삭제하여 불일치 방지
  3. 둘 다 변경되지 않으면 기존 태그 유지

세 가지 케이스가 모두 명확하게 처리되어 데이터 일관성이 보장됩니다.

apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerApiV2.kt (5)

33-56: API 문서에 403 응답이 올바르게 추가되었습니다.

이전 리뷰에서 지적된 생성 API의 403 응답 누락 이슈가 해결되었습니다. USER_BOOK_ACCESS_DENIED 오류가 발생할 수 있는 케이스가 명확하게 문서화되었습니다.


68-86: 상세 조회 API의 403 응답 문서화가 완료되었습니다.

이전 리뷰에서 요청된 소유권 검증 실패 시 403 응답이 API 문서에 추가되었습니다.


125-143: 수정 API의 403 응답 문서화가 완료되었습니다.

소유권 검증 관련 403 응답이 올바르게 추가되었습니다.


155-169: 삭제 API의 403 응답 문서화가 완료되었습니다.

이전 리뷰에서 요청된 403 응답이 API 문서에 추가되었습니다.


114-118: 정렬 파라미터 동작 방식이 명확하게 문서화되었습니다.

Line 115의 문서에서 sort 파라미터가 Pageable의 sort보다 우선 적용된다는 점이 명시되어 있고, Line 118에서도 "Pageable의 sort는 무시됩니다"라고 설명되어 있어 API 사용자가 동작 방식을 이해할 수 있습니다.

이전 리뷰에서 제기된 정렬 파라미터 중복 가능성 우려가 문서화를 통해 해결되었습니다.

apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCaseV2.kt (4)

37-48: 독서 기록 상세 조회 시 소유권 검증이 올바르게 구현되었습니다.

이전 리뷰에서 지적된 보안 취약점이 해결되었습니다:

  1. 사용자 존재 확인
  2. ReadingRecord로부터 userBookId 조회
  3. UserBook 소유권 검증

이 3단계 검증 패턴으로 다른 사용자의 독서 기록에 대한 무단 접근이 차단됩니다.


66-80: 독서 기록 수정 시 소유권 검증이 올바르게 구현되었습니다.

이전 리뷰에서 요청된 소유권 검증이 추가되었습니다. getUserBookIdByReadingRecordId를 통해 간접적으로 소유권을 확인하는 방식이 깔끔하고 효과적입니다.


82-90: 독서 기록 삭제 시 소유권 검증이 올바르게 구현되었습니다.

이전 리뷰에서 지적된 보안 이슈가 해결되었습니다. UserBook 소유권 검증을 통해 권한 없는 삭제가 방지됩니다.


22-35: 생성 API의 소유권 검증이 적절합니다.

userBookService.validateUserBookExists(userBookId, userId)를 통해 사용자가 해당 책에 대한 권한이 있는지 확인하여, 다른 사용자의 책에 독서 기록을 생성하는 것을 방지합니다.

apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerV2.kt (3)

51-66: 기본 정렬 불일치 이슈가 해결되었습니다.

이전 리뷰에서 지적된 인터페이스와 컨트롤러 간 정렬 기준 불일치 문제가 수정되었습니다. Line 56에서 sort = ["updatedAt"]으로 변경되어 API 문서와 실제 동작이 일치합니다.


25-37: 생성 엔드포인트가 올바르게 구현되었습니다.

201 Created 상태 코드를 반환하며, UseCase로 적절하게 위임합니다. 검증 애노테이션도 모두 제자리에 있습니다.


82-89: 삭제 엔드포인트가 HTTP 명세를 준수합니다.

204 No Content 상태 코드를 반환하여 RESTful API 모범 사례를 따릅니다.


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

Comment @coderabbitai help to get the list of available commands and usage tips.

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: 11

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 37c5a8d and ad44b1d.

📒 Files selected for processing (32)
  • apis/src/main/kotlin/org/yapp/apis/emotion/controller/EmotionController.kt
  • apis/src/main/kotlin/org/yapp/apis/emotion/controller/EmotionControllerApi.kt
  • apis/src/main/kotlin/org/yapp/apis/emotion/dto/response/EmotionListResponse.kt
  • apis/src/main/kotlin/org/yapp/apis/emotion/service/EmotionService.kt
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerApiV2.kt
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerV2.kt
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequest.kt
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequestV2.kt
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/UpdateReadingRecordRequestV2.kt
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/ReadingRecordResponse.kt
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/ReadingRecordResponseV2.kt
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordService.kt
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordServiceV2.kt
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCaseV2.kt
  • domain/src/main/kotlin/org/yapp/domain/detailtag/DetailTag.kt
  • domain/src/main/kotlin/org/yapp/domain/detailtag/DetailTagDomainService.kt
  • domain/src/main/kotlin/org/yapp/domain/detailtag/DetailTagRepository.kt
  • domain/src/main/kotlin/org/yapp/domain/readingrecord/PrimaryEmotion.kt
  • domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecord.kt
  • domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt
  • domain/src/main/kotlin/org/yapp/domain/readingrecord/vo/ReadingRecordInfoVO.kt
  • domain/src/main/kotlin/org/yapp/domain/readingrecorddetailtag/ReadingRecordDetailTag.kt
  • domain/src/main/kotlin/org/yapp/domain/readingrecorddetailtag/ReadingRecordDetailTagDomainService.kt
  • domain/src/main/kotlin/org/yapp/domain/readingrecorddetailtag/ReadingRecordDetailTagRepository.kt
  • infra/src/main/kotlin/org/yapp/infra/detailtag/entity/DetailTagEntity.kt
  • infra/src/main/kotlin/org/yapp/infra/detailtag/repository/DetailTagRepositoryImpl.kt
  • infra/src/main/kotlin/org/yapp/infra/detailtag/repository/JpaDetailTagRepository.kt
  • infra/src/main/kotlin/org/yapp/infra/readingrecord/entity/ReadingRecordEntity.kt
  • infra/src/main/kotlin/org/yapp/infra/readingrecorddetailtag/entity/ReadingRecordDetailTagEntity.kt
  • infra/src/main/kotlin/org/yapp/infra/readingrecorddetailtag/repository/JpaReadingRecordDetailTagRepository.kt
  • infra/src/main/kotlin/org/yapp/infra/readingrecorddetailtag/repository/ReadingRecordDetailTagRepositoryImpl.kt
  • infra/src/main/resources/db/migration/mysql/V20251224_001__add_primary_emotion_and_detail_tags.sql
💤 Files with no reviewable changes (1)
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequest.kt
🧰 Additional context used
🧬 Code graph analysis (1)
infra/src/main/kotlin/org/yapp/infra/readingrecorddetailtag/repository/ReadingRecordDetailTagRepositoryImpl.kt (1)
infra/src/main/kotlin/org/yapp/infra/readingrecordtag/repository/impl/ReadingRecordTagRepositoryImpl.kt (1)
  • jpaReadingRecordTagRepository (10-41)
⏰ 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 (30)
apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordService.kt (1)

29-29: 원본 리뷰 댓글은 부정확함

validEmotionTags() 메서드는 CreateReadingRecordRequest에 존재하지 않았으므로 제거되지 않았습니다. CreateReadingRecordRequest의 emotionTags 필드는 @Size(max = 1)@Size(max = 10) 빈 검증 애노테이션으로 보호되어 있습니다. 코드 라인 29의 request.emotionTags는 유효한 구현입니다.

Likely an incorrect or invalid review comment.

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

57-57: 안전한 null 처리 구현 확인

readingRecordInfoVO.pageNumber?.value로 안전한 null 처리가 적절히 구현되었습니다.

apis/src/main/kotlin/org/yapp/apis/emotion/controller/EmotionController.kt (1)

10-21: 컨트롤러 구현 확인

EmotionController 구현이 Spring MVC 모범 사례를 따르고 있습니다. 생성자 주입과 서비스 레이어 위임이 적절합니다.

infra/src/main/kotlin/org/yapp/infra/detailtag/repository/JpaDetailTagRepository.kt (1)

8-11: JPA 리포지토리 메서드 구현 확인

Spring Data JPA 쿼리 메서드 명명 규칙을 올바르게 따르고 있으며, 정렬 순서도 명확히 정의되어 있습니다.

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

3-17: PrimaryEmotion enum 설계 확인

감정 enum 구조가 명확하고 안전하게 설계되었습니다. fromDisplayNamefromCode 팩토리 메서드 모두 OTHER로 안전하게 폴백하여 예외 없는 처리를 보장합니다.

apis/src/main/kotlin/org/yapp/apis/emotion/service/EmotionService.kt (1)

7-15: EmotionService 레이어 구조 확인

애플리케이션 서비스가 도메인 서비스에 적절히 위임하고 응답 변환을 처리하는 깔끔한 레이어 구조를 따르고 있습니다.

apis/src/main/kotlin/org/yapp/apis/emotion/controller/EmotionControllerApi.kt (1)

14-33: API 계약 문서화 확인

OpenAPI 애노테이션을 사용한 API 계약 정의가 포괄적이며, 인터페이스 기반 설계로 구현과 계약을 명확히 분리하고 있습니다.

domain/src/main/kotlin/org/yapp/domain/detailtag/DetailTagDomainService.kt (1)

15-18: 빈 리스트 조기 반환 최적화 확인

빈 입력에 대한 조기 반환으로 불필요한 데이터베이스 호출을 방지하는 좋은 최적화입니다.

infra/src/main/kotlin/org/yapp/infra/readingrecorddetailtag/repository/JpaReadingRecordDetailTagRepository.kt (1)

7-11: 리포지토리 설계 확인

JPA 리포지토리 메서드가 Spring Data 규칙을 따르고 있으며, findByReadingRecordIdIn 메서드를 통한 배치 조회 최적화가 포함되어 있어 N+1 쿼리 문제를 예방할 수 있습니다.

domain/src/main/kotlin/org/yapp/domain/detailtag/DetailTagRepository.kt (1)

6-13: LGTM!

도메인 리포지토리 인터페이스가 깔끔하게 정의되어 있습니다. 표준 CRUD 메서드들이 적절하게 구성되어 있으며, PrimaryEmotion을 통한 조회 메서드도 포함되어 있어 감정 시스템의 요구사항을 잘 반영하고 있습니다.

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

14-34: LGTM!

부분 업데이트를 위한 DTO가 적절하게 설계되어 있습니다. 모든 필드가 nullable로 정의되어 있어 변경이 필요한 필드만 전달할 수 있으며, detailEmotionTagIds가 null일 때 변경하지 않는다는 시맨틱이 스키마 설명에 명확히 문서화되어 있습니다.

domain/src/main/kotlin/org/yapp/domain/readingrecorddetailtag/ReadingRecordDetailTagRepository.kt (1)

5-11: LGTM!

도메인 리포지토리 인터페이스가 적절하게 정의되어 있습니다. 벌크 조회(findByReadingRecordIdIn)와 삭제(deleteAllByReadingRecordId) 메서드가 포함되어 있어 효율적인 데이터 접근을 지원합니다.

domain/src/main/kotlin/org/yapp/domain/readingrecorddetailtag/ReadingRecordDetailTagDomainService.kt (1)

6-33: LGTM!

도메인 서비스가 적절하게 구현되어 있습니다. 빈 리스트에 대한 조기 반환 처리로 불필요한 DB 호출을 방지하고 있으며, createAndSaveAll 메서드에서 도메인 객체 생성과 저장이 깔끔하게 분리되어 있습니다.

domain/src/main/kotlin/org/yapp/domain/detailtag/DetailTag.kt (1)

8-58: LGTM!

도메인 엔티티가 잘 설계되어 있습니다. private 생성자와 팩토리 메서드를 통한 생성 제어, create()에서의 유효성 검증, 영속성 레이어를 위한 reconstruct() 분리, 그리고 Id value class를 통한 타입 안전성이 모두 적절하게 구현되어 있습니다.

infra/src/main/kotlin/org/yapp/infra/detailtag/repository/DetailTagRepositoryImpl.kt (1)

10-45: LGTM!

리포지토리 구현이 표준 어댑터 패턴을 따르고 있습니다. JPA 리포지토리에 대한 위임과 도메인/엔티티 간 매핑이 깔끔하게 처리되어 있으며, findById에서 Optional 처리도 적절합니다.

infra/src/main/kotlin/org/yapp/infra/readingrecorddetailtag/entity/ReadingRecordDetailTagEntity.kt (2)

14-73: LGTM!

JPA 엔티티가 잘 구현되어 있습니다:

  • 복합 유니크 제약조건과 인덱스가 적절하게 설정되어 있습니다.
  • Soft delete 패턴이 @SQLDelete@SQLRestriction을 통해 올바르게 구현되어 있습니다.
  • 도메인 객체와의 매핑 메서드(toDomain, fromDomain)가 깔끔하게 제공되고 있습니다.
  • equals/hashCode가 ID 기반으로 올바르게 구현되어 있습니다.

7-9: 불필요한 import를 정리하세요.

DetailTagReadingRecord 클래스가 import되어 있지만, 실제로는 해당 클래스의 Id nested class만 사용되고 있습니다. 이는 정상적으로 동작하지만, IDE에서 unused import 경고가 발생할 수 있습니다.

⛔ Skipped due to learnings
Learnt from: move-hoon
Repo: YAPP-Github/Reed-Server PR: 132
File: infra/src/main/resources/db/migration/mysql/V20251115_001__add_notification_and_device_tables.sql:7-16
Timestamp: 2025-11-15T09:34:11.134Z
Learning: Reed-Server 프로젝트는 데이터베이스 레벨에서 외래키(Foreign Key) 제약조건을 사용하지 않는 정책을 따르고 있습니다. Entity 클래스에서도 JPA 관계 매핑(ManyToOne, JoinColumn 등) 없이 단순 ID 값으로만 참조합니다.
infra/src/main/kotlin/org/yapp/infra/detailtag/entity/DetailTagEntity.kt (1)

28-29: PrimaryEmotion enum 값들의 길이 검증 완료 - 문제 없음

PrimaryEmotion enum의 모든 값(WARMTH, JOY, SADNESS, INSIGHT, OTHER)이 최대 7자로, 컬럼의 20자 제한을 충분히 만족합니다. 추가 조치가 필요하지 않습니다.

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

59-59: emptyList() 할당은 의도된 설계입니다.

emotionTagsreading_records 테이블에 저장되지 않고 별도의 reading_record_tags 테이블에 정규화되어 있습니다. 따라서 Entity 계층의 toDomain() 메서드에서 빈 리스트를 반환하는 것은 올바른 설계입니다. 서비스 계층의 buildReadingRecordInfoVO() 메서드에서 readingRecordTagRepository를 통해 태그를 별도로 조회하여 ReadingRecordInfoVO에 정상적으로 채우고 있으므로 데이터 손실은 없습니다.

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

25-57: API 인터페이스 구조 적절함

V2 API 인터페이스가 잘 정의되어 있습니다. OpenAPI 문서화가 충실하고, REST 컨벤션을 따르고 있습니다.

infra/src/main/resources/db/migration/mysql/V20251224_001__add_primary_emotion_and_detail_tags.sql (2)

50-52: 외래 키 삭제 동작 불일치 확인 필요

fk_rrdt_reading_recordON DELETE CASCADE가 있지만, fk_rrdt_detail_tag에는 삭제 동작이 정의되지 않았습니다.

detail_tags가 마스터 데이터로 삭제되지 않을 예정이라면 현재 설정이 적절합니다. 그러나 향후 detail_tags 삭제 시 reading_record_detail_tags에 고아 레코드가 남거나 FK 제약 위반이 발생할 수 있습니다. 의도된 설계인지 확인해 주세요.


8-23: 기존 데이터 마이그레이션 로직 적절함

기존 태그를 primary_emotion으로 매핑하는 로직이 적절합니다. LIMIT 1로 첫 번째 태그만 사용하고, 태그가 없는 경우 COALESCE로 'OTHER'를 기본값으로 설정합니다.

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

27-54: V2 생성 메서드 구조 적절함

V2 API를 위한 간결한 CRUD 메서드가 잘 구현되어 있습니다. 도메인 로직과 영속성 계층이 적절히 분리되어 있고, 읽기 기록 카운트 증가 로직도 포함되어 있습니다.


128-131: 레거시 감정 태그 매핑이 올바르게 구현됨

PrimaryEmotion.fromDisplayName() 메서드는 레거시 태그명('따뜻함', '즐거움', '슬픔', '깨달음')을 올바르게 처리합니다. enum의 displayName 값이 레거시 태그명과 정확히 일치하며, 매칭되지 않는 경우 안전하게 OTHER로 폴백되므로 문제가 없습니다.

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

9-24: VO 확장 적절함

primaryEmotiondetailEmotions 필드 추가가 적절하게 이루어졌습니다. pageNumber의 nullable 변경도 V2 요구사항과 일치합니다.


60-63: DetailEmotionInfo 중첩 클래스 적절함

VO 내부에 관련 상세 정보 클래스를 중첩하는 것은 적절한 구조입니다.

apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordServiceV2.kt (2)

144-147: 삭제 순서 적절함

detail tags를 먼저 삭제하고 reading record를 삭제하는 순서가 올바릅니다. 다만, reading_record_detail_tags 테이블에 ON DELETE CASCADE가 설정되어 있으므로, detail tags 삭제는 불필요할 수 있습니다. 명시적 삭제가 의도된 것이라면 현재 방식도 괜찮습니다.


68-102: 목록 조회 시 배치 쿼리 패턴 적절함

N+1 문제를 피하기 위해 reading record IDs를 수집하고 한 번에 detail tags를 조회하는 패턴이 잘 구현되어 있습니다.

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

74-76: update에서 pageNumber를 null로 설정 불가

현재 update() 로직에서 pageNumbernull로 전달되면 기존 값이 유지됩니다. 만약 V2에서 페이지 번호를 제거(null로 설정)해야 하는 요구사항이 있다면, 현재 구현으로는 불가능합니다.

명시적으로 null을 설정할 수 있도록 하려면 별도의 플래그나 Optional 패턴을 고려해야 합니다. 현재 요구사항에서 페이지 번호 제거가 필요 없다면 무시해도 됩니다.


20-38: 도메인 모델 확장 적절함

primaryEmotion 필드 추가와 pageNumber nullable 변경이 V2 요구사항에 맞게 잘 구현되었습니다. factory method와 reconstruct method 모두 새 필드를 적절히 처리합니다.

Copy link
Member

@minwoo1999 minwoo1999 left a comment

Choose a reason for hiding this comment

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

고생하셨습니다 :)

변경 사항:
- primaryEmotion 변경 + 새 detailTags 제공 → 새 태그로 교체
- primaryEmotion 변경 + detailTags 미제공 → 기존 태그 삭제 (불일치 방지)
- primaryEmotion 유지 + 새 detailTags 제공 → 새 태그로 교체
- primaryEmotion 유지 + detailTags 미제공 → 기존 태그 유지
리팩토링:
- updateDetailEmotionTags() private 메서드로 분리
- isPrimaryEmotionChanged() 헬퍼 메서드 추가
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

♻️ Duplicate comments (1)
apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequestV2.kt (1)

15-23: 스키마 예제의 UUID 형식이 유효하지 않습니다.

"uuid-1", "uuid-2"는 유효한 UUID 형식이 아닙니다. API 문서의 정확성을 위해 실제 UUID 형식의 예제를 사용하세요.

🔎 제안하는 수정 사항
     example = """
         {
           "pageNumber": 42,
           "quote": "이것은 기억에 남는 문장입니다.",
           "review": "이 책은 매우 인상적이었습니다.",
           "primaryEmotion": "JOY",
-          "detailEmotionTagIds": ["uuid-1", "uuid-2"]
+          "detailEmotionTagIds": ["550e8400-e29b-41d4-a716-446655440000", "6ba7b810-9dad-11d1-80b4-00c04fd430c8"]
         }
     """
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ad44b1d and ab30e00.

📒 Files selected for processing (12)
  • apis/src/main/kotlin/org/yapp/apis/book/exception/UserBookErrorCode.kt
  • apis/src/main/kotlin/org/yapp/apis/book/service/UserBookService.kt
  • apis/src/main/kotlin/org/yapp/apis/emotion/dto/response/EmotionListResponse.kt
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerApiV2.kt
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequestV2.kt
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/UpdateReadingRecordRequestV2.kt
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/ReadingRecordResponse.kt
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/ReadingRecordResponseV2.kt
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordService.kt
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordServiceV2.kt
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCase.kt
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCaseV2.kt
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-17T15:09:36.267Z
Learnt from: hoonyworld
Repo: YAPP-Github/Reed-Server 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() 메서드를 사용해야 합니다.

Applied to files:

  • apis/src/main/kotlin/org/yapp/apis/emotion/dto/response/EmotionListResponse.kt
⏰ 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 (16)
apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerApiV2.kt (2)

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

생성 엔드포인트의 검증 어노테이션, 반환 타입, HTTP 상태 코드가 모두 적절하게 구성되어 있습니다.


83-109: 정렬 파라미터 우선순위가 문서화되었습니다.

이전 리뷰 코멘트에서 지적된 sort 파라미터와 Pageable 정렬의 우선순위 문제가 API 문서에 명확히 기재되었습니다(85번 라인, 105-106번 라인, 108번 라인). 이제 클라이언트가 동작을 명확히 이해할 수 있습니다.

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

11-11: API 계층의 새로운 접근 권한 거부 에러 코드 추가

이 변경은 API 계층에 새로운 에러 코드 USER_BOOK_ACCESS_DENIED(403)을 추가하는 것으로, 도메인 계층의 USER_BOOK_NOT_FOUND(404)와는 별도의 열거형입니다.

정보 보안 측면의 고려:

  • 403 FORBIDDEN은 리소스가 존재하지만 접근 권한이 없음을 암시합니다.
  • 404 NOT_FOUND는 리소스 존재 여부를 숨겨 열거 공격을 방지합니다.
  • 이는 REST API 설계상 의도된 의미 있는 구분입니다.

UUID 기반 ID 사용으로 인한 열거 공격 위험이 낮으므로, 이 설계 선택은 타당합니다.

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

29-29: 검증 결과 validEmotionTags() 메서드는 코드베이스에 존재하지 않으므로 "제거되었다"는 전제가 올바르지 않습니다. 감정 태그 검증은 이미 다른 계층에서 수행되고 있습니다:

  1. DTO 계층: CreateReadingRecordRequestemotionTags 필드는 @Size(max = 1) (리스트 크기)와 @Size(max = 10) (각 요소 길이) 검증 어노테이션이 적용되어 있습니다.
  2. 도메인 계층: EmotionTag.newInstance()에서 isNotBlank() 및 길이 검증이 수행됩니다.

따라서 라인 29의 request.emotionTags 직접 전달은 적절하며, 검증 로직은 유지되고 있습니다.

apis/src/main/kotlin/org/yapp/apis/emotion/dto/response/EmotionListResponse.kt (2)

8-71: 구현이 잘 되었습니다!

  • Private constructor + companion object factory method 패턴이 V2 DTO 전체에 일관되게 적용되었습니다 (과거 리뷰 코멘트 반영 확인).
  • 불변성을 위해 모든 프로퍼티가 val로 선언되었습니다 (learnings 기반).
  • Swagger 스키마 어노테이션이 적절히 추가되어 API 문서화가 잘 되어 있습니다.
  • sortedBy { it.displayOrder }를 통해 세부 감정의 정렬 순서가 보장됩니다.

14-29: PrimaryEmotion.entries를 통해 모든 대분류를 반환하는 것이 의도된 동작인지 확인이 필요합니다.

마이그레이션 데이터를 분석한 결과:

  • 4개 감정(즐거움, 따뜻함, 슬픔, 깨달음)만 세부감정 데이터 포함
  • 기타(OTHER) 감정은 마이그레이션에서 세부감정이 없음
  • 코드는 PrimaryEmotion.entries.map으로 모든 5개 감정을 반환하므로, 기타 감정은 항상 빈 detailEmotions 리스트로 응답됨

이 동작이 의도된 것이라면 문제없습니다(UI 일관성을 위해 항상 완전한 감정 분류 체계를 제공). 그러나 비즈니스 요구사항 관점에서 명시적으로 확인 필요합니다.

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

14-34: LGTM! 부분 업데이트 설계가 적절합니다.

모든 필드가 nullable로 설계되어 부분 업데이트를 올바르게 지원하고 있습니다. detailEmotionTagIdsnull이면 변경하지 않는다는 의미가 스키마 설명에 명확히 문서화되어 있어 좋습니다.

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

20-21: LGTM! pageNumber nullable 처리가 올바릅니다.

필드 타입을 Int?로 변경하고, 스키마 설명에 "(선택)"을 추가했으며, 매핑 로직도 안전한 호출(?.)을 사용하여 일관성 있게 구현되었습니다.

Also applies to: 57-57

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

12-51: LGTM! V2 응답 DTO 설계가 우수합니다.

private 생성자와 정적 팩토리 메서드 패턴을 사용하여 인스턴스 생성을 제어하고, 중첩된 DTO들(PrimaryEmotionDto, DetailEmotionDto)도 동일한 패턴을 일관성 있게 적용했습니다. 모든 필드에 Swagger 스키마 어노테이션이 적절히 작성되어 있습니다.


55-76: 매핑 로직이 정확합니다.

ReadingRecordInfoVO로부터 응답 DTO로의 변환이 올바르게 구현되었습니다. primaryEmotiondetailEmotions를 각각의 중첩 DTO로 변환하는 로직이 명확하고, ISO_LOCAL_DATE_TIME 포맷터를 재사용하여 일관된 타임스탬프 형식을 보장합니다.

apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCaseV2.kt (3)

39-50: 소유권 검증이 올바르게 구현되었습니다.

getReadingRecordDetail 메서드가 이제 getUserBookIdByReadingRecordId를 호출하여 연관된 userBookId를 가져온 후, validateUserBookExists로 소유권을 검증합니다. 이전 리뷰에서 지적된 보안 취약점이 해결되었습니다.


68-83: 소유권 검증이 올바르게 구현되었습니다.

updateReadingRecord 메서드도 동일한 패턴으로 소유권을 검증합니다. 수정 작업 전에 대상 독서 기록이 요청한 사용자의 것인지 확인하여 무단 수정을 방지합니다.


85-94: 소유권 검증이 올바르게 구현되었습니다.

deleteReadingRecord 메서드도 삭제 전에 소유권을 검증하여 다른 사용자의 독서 기록을 삭제할 수 없도록 보호합니다. 이전 리뷰의 보안 취약점이 모두 해결되었습니다.

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

164-189: primaryEmotion 변경 시 데이터 불일치 문제가 해결되었습니다.

이전 리뷰에서 지적된 문제가 updateDetailEmotionTags 메서드를 통해 올바르게 해결되었습니다:

  • newDetailTagIds가 제공되면 기존 태그를 삭제하고 새 태그로 교체
  • primaryEmotionChanged가 true이고 새 태그가 제공되지 않으면 기존 태그를 모두 삭제
  • 둘 다 아니면 기존 태그 유지

이 로직은 primaryEmotion이 변경될 때 일치하지 않는 세부 감정 태그가 남아있는 것을 방지합니다.


26-56: createReadingRecord 구현이 우수합니다.

독서 기록 생성 로직이 잘 구조화되어 있습니다:

  1. 세부 감정 태그 유효성 검증
  2. 독서 기록 생성
  3. 세부 감정 태그 연관 저장
  4. 사용자 lastActivity 업데이트
  5. 응답 생성

각 단계가 명확하게 분리되어 있고, 트랜잭션 일관성이 보장됩니다.


68-102: 페이징 조회 시 N+1 문제를 효과적으로 방지했습니다.

getReadingRecordsByDynamicCondition 메서드가 다음과 같이 최적화되었습니다:

  • 모든 독서 기록 ID를 한 번에 수집
  • findByReadingRecordIdIn으로 일괄 조회
  • detail tag도 일괄 조회 후 메모리에서 그룹화

이는 N+1 쿼리 문제를 방지하는 효율적인 구현입니다.

@move-hoon move-hoon changed the title Book 480 feature/#145 feat: 독서 기록 V2 API 및 대분류/세부 감정 시스템 구현 Dec 30, 2025
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: 4

♻️ Duplicate comments (2)
apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordServiceV2.kt (1)

145-155: require() 예외 처리 확인 필요

require()IllegalArgumentException을 던지는데, 이 예외가 적절한 HTTP 400 응답으로 변환되는지 확인이 필요합니다. 글로벌 예외 핸들러에서 처리되지 않으면 500 에러로 반환될 수 있습니다.

#!/bin/bash
# 글로벌 예외 핸들러에서 IllegalArgumentException 처리 확인
rg -n "IllegalArgumentException" --type kotlin -A 3

에러 메시지 한글화는 이전 리뷰에서 제안된 nitpick 사항입니다.

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

15-23: 스키마 예제의 UUID 형식이 유효하지 않습니다.

"uuid-1", "uuid-2"는 유효한 UUID 형식이 아닙니다. API 문서의 정확성을 위해 실제 UUID 형식의 예제를 사용해 주세요.

🔎 수정 제안
           "primaryEmotion": "JOY",
-          "detailEmotionTagIds": ["uuid-1", "uuid-2"]
+          "detailEmotionTagIds": ["550e8400-e29b-41d4-a716-446655440000", "6ba7b810-9dad-11d1-80b4-00c04fd430c8"]
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ab30e00 and ed4a5b7.

📒 Files selected for processing (4)
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerApiV2.kt
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerV2.kt
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequestV2.kt
  • apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordServiceV2.kt
⏰ 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 (15)
apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordServiceV2.kt (5)

58-66: LGTM!

독서 기록과 세부 태그를 조회하여 응답을 구성하는 로직이 적절합니다.


68-102: 효율적인 배치 조회 구현

N+1 쿼리 문제를 방지하기 위해 findByReadingRecordIdIn으로 배치 조회하고, associateBy로 O(1) 룩업을 구현한 점이 좋습니다.


104-138: 이전 리뷰 이슈가 해결되었습니다.

primaryEmotion 변경 시 detailEmotionTagIds가 제공되지 않으면 기존 태그를 삭제하는 로직이 updateDetailEmotionTags 헬퍼 메서드(lines 164-189)에 올바르게 구현되었습니다.


140-143: LGTM!

세부 태그를 먼저 삭제한 후 독서 기록을 삭제하여 FK 제약 조건 위반을 방지합니다.


164-189: LGTM!

세 가지 케이스를 명확하게 처리합니다:

  1. 새 태그 제공 시 → 기존 삭제 후 새 태그 저장
  2. primaryEmotion만 변경 시 → 기존 태그 삭제
  3. 그 외 → 기존 태그 유지
apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequestV2.kt (2)

25-46: LGTM!

필드 검증이 적절하게 구성되었습니다. @NotNullprimaryEmotion에 추가되어 이전 리뷰 피드백이 반영되었습니다.


48-49: 검증 헬퍼 메서드 유지

Jakarta Validation이 먼저 실행되므로 이 메서드들이 호출될 때는 이미 non-null이 보장됩니다. 하지만 방어적 코딩으로서 유지해도 무방합니다.

apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerApiV2.kt (4)

59-86: LGTM!

이전 리뷰 피드백이 반영되어 403 응답이 문서화되었습니다.


88-114: LGTM!

정렬 파라미터 우선순위가 API 설명에 명확하게 문서화되었습니다: "sort 파라미터가 지정된 경우 해당 정렬이 우선 적용되며, 지정하지 않으면 기본 정렬(updatedAt DESC)이 적용됩니다."


116-144: LGTM!

403 응답이 문서화되어 이전 리뷰 피드백이 반영되었습니다.


146-169: LGTM!

삭제 API에 403 응답이 문서화되어 이전 리뷰 피드백이 반영되었습니다.

apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerV2.kt (4)

19-37: LGTM!

리소스 생성에 적절한 HTTP 201 상태 코드를 반환합니다.


39-49: LGTM!

사용자 인증과 함께 use case로 위임하는 깔끔한 구현입니다.


68-80: LGTM!

수정 API가 적절하게 구현되었습니다.


82-89: LGTM!

삭제 성공 시 HTTP 204 No Content를 적절하게 반환합니다.

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
4.6% Coverage on New Code (required ≥ 80%)
4.2% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@move-hoon move-hoon merged commit 20e8e65 into develop Dec 30, 2025
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BOOK-480/feat] 상세 감정 기록 기능 구현

3 participants