-
Notifications
You must be signed in to change notification settings - Fork 1
Book 480 feature/#145 #146
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
base: develop
Are you sure you want to change the base?
Conversation
Walkthrough상세 감정 태그 기능을 추가합니다. DetailTag 도메인 엔티티, Emotion API 엔드포인트, ReadingRecord V2 API(생성/조회/수정/삭제)를 구현하고, 데이터베이스 스키마를 확장하여 독서 기록과 상세 감정 태그의 연관을 지원합니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant EmotionController
participant EmotionService
participant DetailTagDomainService
participant DetailTagRepository
participant Database
Client->>EmotionController: GET /api/v2/emotions
activate EmotionController
EmotionController->>EmotionService: getEmotionList()
activate EmotionService
EmotionService->>DetailTagDomainService: findAll()
activate DetailTagDomainService
DetailTagDomainService->>DetailTagRepository: findAll()
activate DetailTagRepository
DetailTagRepository->>Database: SELECT * FROM detail_tags
activate Database
Database-->>DetailTagRepository: List<DetailTagEntity>
deactivate Database
DetailTagRepository-->>DetailTagDomainService: List<DetailTag>
deactivate DetailTagRepository
DetailTagDomainService-->>EmotionService: List<DetailTag>
deactivate DetailTagDomainService
rect rgb(200, 220, 255)
Note over EmotionService: PrimaryEmotion별로 grouping<br/>EmotionListResponse 생성
end
EmotionService-->>EmotionController: EmotionListResponse
deactivate EmotionService
EmotionController-->>Client: ResponseEntity<200, EmotionListResponse>
deactivate EmotionController
sequenceDiagram
participant Client
participant ReadingRecordControllerV2
participant ReadingRecordUseCaseV2
participant UserService
participant UserBookService
participant ReadingRecordServiceV2
participant ReadingRecordDomainService
participant DetailTagDomainService
participant ReadingRecordDetailTagDomainService
participant Database
Client->>ReadingRecordControllerV2: POST /api/v2/reading-records/{userBookId}
activate ReadingRecordControllerV2
ReadingRecordControllerV2->>ReadingRecordUseCaseV2: createReadingRecord(userId, userBookId, request)
activate ReadingRecordUseCaseV2
rect rgb(220, 240, 220)
Note over ReadingRecordUseCaseV2: 사용자 검증
ReadingRecordUseCaseV2->>UserService: findById(userId)
activate UserService
UserService->>Database: SELECT * FROM users
Database-->>UserService: User
UserService-->>ReadingRecordUseCaseV2: User
deactivate UserService
end
rect rgb(220, 240, 220)
Note over ReadingRecordUseCaseV2: 사용자-책 관계 검증
ReadingRecordUseCaseV2->>UserBookService: findById(userBookId)
activate UserBookService
UserBookService->>Database: SELECT * FROM user_books
Database-->>UserBookService: UserBook
UserBookService-->>ReadingRecordUseCaseV2: UserBook
deactivate UserBookService
end
ReadingRecordUseCaseV2->>ReadingRecordServiceV2: createReadingRecord(userId, userBookId, request)
activate ReadingRecordServiceV2
rect rgb(255, 240, 220)
Note over ReadingRecordServiceV2: 상세 감정 태그 검증
ReadingRecordServiceV2->>DetailTagDomainService: findAllById(detailEmotionTagIds)
activate DetailTagDomainService
DetailTagDomainService->>Database: SELECT * FROM detail_tags
Database-->>DetailTagDomainService: List<DetailTag>
DetailTagDomainService-->>ReadingRecordServiceV2: List<DetailTag>
deactivate DetailTagDomainService
end
rect rgb(255, 240, 220)
Note over ReadingRecordServiceV2: 읽기 기록 생성 및 저장
ReadingRecordServiceV2->>ReadingRecordDomainService: createReadingRecordV2(...)
activate ReadingRecordDomainService
ReadingRecordDomainService->>Database: INSERT INTO reading_records
Database-->>ReadingRecordDomainService: ReadingRecord
ReadingRecordDomainService-->>ReadingRecordServiceV2: ReadingRecord
deactivate ReadingRecordDomainService
end
rect rgb(255, 240, 220)
Note over ReadingRecordServiceV2: 상세 감정 태그 연관 생성
ReadingRecordServiceV2->>ReadingRecordDetailTagDomainService: createAndSaveAll(readingRecordId, detailEmotionTagIds)
activate ReadingRecordDetailTagDomainService
ReadingRecordDetailTagDomainService->>Database: INSERT INTO reading_record_detail_tags
Database-->>ReadingRecordDetailTagDomainService: List<ReadingRecordDetailTag>
ReadingRecordDetailTagDomainService-->>ReadingRecordServiceV2: List<ReadingRecordDetailTag>
deactivate ReadingRecordDetailTagDomainService
end
rect rgb(200, 220, 255)
Note over ReadingRecordServiceV2: 응답 구성
end
ReadingRecordServiceV2-->>ReadingRecordUseCaseV2: ReadingRecordResponseV2
deactivate ReadingRecordServiceV2
ReadingRecordUseCaseV2-->>ReadingRecordControllerV2: ReadingRecordResponseV2
deactivate ReadingRecordUseCaseV2
ReadingRecordControllerV2-->>Client: ResponseEntity<201, ReadingRecordResponseV2>
deactivate ReadingRecordControllerV2
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (2 warnings, 1 inconclusive)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. 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.
Actionable comments posted: 11
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (32)
apis/src/main/kotlin/org/yapp/apis/emotion/controller/EmotionController.ktapis/src/main/kotlin/org/yapp/apis/emotion/controller/EmotionControllerApi.ktapis/src/main/kotlin/org/yapp/apis/emotion/dto/response/EmotionListResponse.ktapis/src/main/kotlin/org/yapp/apis/emotion/service/EmotionService.ktapis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerApiV2.ktapis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerV2.ktapis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequest.ktapis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequestV2.ktapis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/UpdateReadingRecordRequestV2.ktapis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/ReadingRecordResponse.ktapis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/ReadingRecordResponseV2.ktapis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordService.ktapis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordServiceV2.ktapis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCaseV2.ktdomain/src/main/kotlin/org/yapp/domain/detailtag/DetailTag.ktdomain/src/main/kotlin/org/yapp/domain/detailtag/DetailTagDomainService.ktdomain/src/main/kotlin/org/yapp/domain/detailtag/DetailTagRepository.ktdomain/src/main/kotlin/org/yapp/domain/readingrecord/PrimaryEmotion.ktdomain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecord.ktdomain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.ktdomain/src/main/kotlin/org/yapp/domain/readingrecord/vo/ReadingRecordInfoVO.ktdomain/src/main/kotlin/org/yapp/domain/readingrecorddetailtag/ReadingRecordDetailTag.ktdomain/src/main/kotlin/org/yapp/domain/readingrecorddetailtag/ReadingRecordDetailTagDomainService.ktdomain/src/main/kotlin/org/yapp/domain/readingrecorddetailtag/ReadingRecordDetailTagRepository.ktinfra/src/main/kotlin/org/yapp/infra/detailtag/entity/DetailTagEntity.ktinfra/src/main/kotlin/org/yapp/infra/detailtag/repository/DetailTagRepositoryImpl.ktinfra/src/main/kotlin/org/yapp/infra/detailtag/repository/JpaDetailTagRepository.ktinfra/src/main/kotlin/org/yapp/infra/readingrecord/entity/ReadingRecordEntity.ktinfra/src/main/kotlin/org/yapp/infra/readingrecorddetailtag/entity/ReadingRecordDetailTagEntity.ktinfra/src/main/kotlin/org/yapp/infra/readingrecorddetailtag/repository/JpaReadingRecordDetailTagRepository.ktinfra/src/main/kotlin/org/yapp/infra/readingrecorddetailtag/repository/ReadingRecordDetailTagRepositoryImpl.ktinfra/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 구조가 명확하고 안전하게 설계되었습니다.
fromDisplayName과fromCode팩토리 메서드 모두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()분리, 그리고Idvalue 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를 정리하세요.
DetailTag와ReadingRecord클래스가 import되어 있지만, 실제로는 해당 클래스의Idnested 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 값들의 길이 검증 완료 - 문제 없음
PrimaryEmotionenum의 모든 값(WARMTH, JOY, SADNESS, INSIGHT, OTHER)이 최대 7자로, 컬럼의 20자 제한을 충분히 만족합니다. 추가 조치가 필요하지 않습니다.infra/src/main/kotlin/org/yapp/infra/readingrecord/entity/ReadingRecordEntity.kt (1)
59-59: emptyList() 할당은 의도된 설계입니다.
emotionTags는reading_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_record는ON 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 확장 적절함
primaryEmotion과detailEmotions필드 추가가 적절하게 이루어졌습니다.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()로직에서pageNumber가null로 전달되면 기존 값이 유지됩니다. 만약 V2에서 페이지 번호를 제거(null로 설정)해야 하는 요구사항이 있다면, 현재 구현으로는 불가능합니다.명시적으로 null을 설정할 수 있도록 하려면 별도의 플래그나 Optional 패턴을 고려해야 합니다. 현재 요구사항에서 페이지 번호 제거가 필요 없다면 무시해도 됩니다.
20-38: 도메인 모델 확장 적절함
primaryEmotion필드 추가와pageNumbernullable 변경이 V2 요구사항에 맞게 잘 구현되었습니다. factory method와 reconstruct method 모두 새 필드를 적절히 처리합니다.
| @RequestParam(required = false) @Parameter(description = "정렬 타입") sort: ReadingRecordSortType?, | ||
| @PageableDefault(size = 10, sort = ["createdAt"], direction = Sort.Direction.DESC) | ||
| @Parameter(description = "페이징 정보") pageable: Pageable | ||
| ): ResponseEntity<Page<ReadingRecordResponseV2>> |
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 | 🔵 Trivial
정렬 파라미터 중복 가능성 검토 필요
sort 파라미터(ReadingRecordSortType)와 Pageable의 기본 정렬(createdAt DESC)이 함께 존재합니다. 두 정렬 옵션이 동시에 전달될 경우 어떤 것이 우선하는지 명확하지 않습니다.
ReadingRecordSortType이 비즈니스 로직에서 우선 적용된다면, Pageable의 sort 파라미터는 무시되어야 합니다. API 문서에 이 동작을 명시하거나, Pageable에서 sort를 제외하는 것을 고려해 주세요.
🤖 Prompt for AI Agents
In
apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerApiV2.kt
around lines 104 to 107, the request accepts both a ReadingRecordSortType (sort)
and a Pageable with a default sort (createdAt DESC), causing ambiguity when both
are present; change handling so ReadingRecordSortType takes precedence by
ignoring Pageable.sort when sort != null: remove or neutralize the sort in the
@PageableDefault (or stop relying on its sort), and in the controller construct
a new Pageable (PageRequest.of(pageable.pageNumber, pageable.pageSize,
sortFromEnum)) when sort is provided, otherwise use the incoming pageable as-is;
alternatively document explicitly in the API that ReadingRecordSortType
overrides Pageable.sort if you prefer not to change code.
| example = """ | ||
| { | ||
| "pageNumber": 42, | ||
| "quote": "이것은 기억에 남는 문장입니다.", | ||
| "review": "이 책은 매우 인상적이었습니다.", | ||
| "primaryEmotion": "JOY", | ||
| "detailEmotionTagIds": ["uuid-1", "uuid-2"] | ||
| } | ||
| """ |
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 | 🔵 Trivial
스키마 예제의 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"]
}
"""📝 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.
| example = """ | |
| { | |
| "pageNumber": 42, | |
| "quote": "이것은 기억에 남는 문장입니다.", | |
| "review": "이 책은 매우 인상적이었습니다.", | |
| "primaryEmotion": "JOY", | |
| "detailEmotionTagIds": ["uuid-1", "uuid-2"] | |
| } | |
| """ | |
| example = """ | |
| { | |
| "pageNumber": 42, | |
| "quote": "이것은 기억에 남는 문장입니다.", | |
| "review": "이 책은 매우 인상적이었습니다.", | |
| "primaryEmotion": "JOY", | |
| "detailEmotionTagIds": ["550e8400-e29b-41d4-a716-446655440000", "6ba7b810-9dad-11d1-80b4-00c04fd430c8"] | |
| } | |
| """ |
🤖 Prompt for AI Agents
In
apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequestV2.kt
around lines 14 to 22, the schema example uses invalid UUID placeholders
("uuid-1", "uuid-2"); replace them with properly formatted UUID strings
(hyphenated 8-4-4-4-12 hex groups, e.g. 123e4567-e89b-12d3-a456-426614174000) so
the API documentation shows valid UUID examples.
| @field:Schema(description = "대분류 감정", example = "JOY", required = true) | ||
| val primaryEmotion: PrimaryEmotion? = 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.
primaryEmotion 필드에 @NotNull 검증 어노테이션이 누락되어 있습니다.
스키마에서 required = true로 명시되어 있지만, 실제 검증 어노테이션이 없어서 컨트롤러 레벨에서 validation이 수행되지 않습니다. null 값이 전달되면 서비스 레이어의 validPrimaryEmotion()에서 런타임 예외가 발생하게 됩니다.
🔎 제안하는 수정 사항
+ @field:NotNull(message = "대분류 감정은 필수입니다.")
@field:Schema(description = "대분류 감정", example = "JOY", required = true)
val primaryEmotion: PrimaryEmotion? = null,그리고 import 추가:
import jakarta.validation.constraints.NotNull📝 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.
| @field:Schema(description = "대분류 감정", example = "JOY", required = true) | |
| val primaryEmotion: PrimaryEmotion? = null, | |
| @field:NotNull(message = "대분류 감정은 필수입니다.") | |
| @field:Schema(description = "대분류 감정", example = "JOY", required = true) | |
| val primaryEmotion: PrimaryEmotion? = null, |
🤖 Prompt for AI Agents
In
apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequestV2.kt
around lines 40-41, the primaryEmotion property is marked required in the Schema
but lacks a runtime validation annotation; add
jakarta.validation.constraints.NotNull to the primaryEmotion declaration and
import jakarta.validation.constraints.NotNull at the top of the file so
controller-level validation will reject nulls before reaching service layer.
| @field:Schema(description = "현재 읽은 페이지 번호", example = "42") | ||
| val pageNumber: Int, | ||
| val pageNumber: Int?, |
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.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Explore repository structure to understand the project layout
find . -type f -name "*.kt" | grep -E "(api|version|response)" | head -20Repository: YAPP-Github/Reed-Server
Length of output: 1569
🏁 Script executed:
#!/bin/bash
# Look at the specific file mentioned in the review
cat -n apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/ReadingRecordResponse.kt 2>/dev/null || echo "File not found, searching for it..."
fd -e kt "ReadingRecordResponse"Repository: YAPP-Github/Reed-Server
Length of output: 3388
🏁 Script executed:
#!/bin/bash
# Check the overall directory structure
ls -la apis/ 2>/dev/null || ls -laRepository: YAPP-Github/Reed-Server
Length of output: 360
🏁 Script executed:
#!/bin/bash
# Check for V2 response file
cat -n apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/ReadingRecordResponseV2.ktRepository: YAPP-Github/Reed-Server
Length of output: 4326
🏁 Script executed:
#!/bin/bash
# Search for ReadingRecordResponse usage
rg -n "ReadingRecordResponse" --type kotlin -g '!**/test/**' -C 2Repository: YAPP-Github/Reed-Server
Length of output: 38444
🏁 Script executed:
#!/bin/bash
# Check for API versioning patterns (controller classes)
fd -e kt "controller" | grep -i "readingrecord" | head -10Repository: YAPP-Github/Reed-Server
Length of output: 424
🏁 Script executed:
#!/bin/bash
# Search for V1/V2 API patterns to understand versioning strategy
rg -n "@GetMapping|@PostMapping|@PutMapping|@DeleteMapping" apis/src/main/kotlin/org/yapp/apis/readingrecord/ --type kotlin -C 1Repository: YAPP-Github/Reed-Server
Length of output: 7225
🏁 Script executed:
#!/bin/bash
# Check if there are any git history or version indicators for ReadingRecordResponse
# to determine if pageNumber was previously non-nullable
rg -n "pageNumber" apis/src/main/kotlin/org/yapp/apis/readingrecord/ --type kotlin -B 1 -A 1 | grep -E "(val|var|pageNumber|Int\?|Int\b)" | head -30Repository: YAPP-Github/Reed-Server
Length of output: 3173
🏁 Script executed:
#!/bin/bash
# Check if there are any deprecated versions or comments about changes
rg -n "@Deprecated|breaking|migration" apis/src/main/kotlin/org/yapp/apis/readingrecord/ --type kotlinRepository: YAPP-Github/Reed-Server
Length of output: 49
🏁 Script executed:
#!/bin/bash
# Check ReadingRecordInfoVO to confirm nullable design
fd -e kt "ReadingRecordInfoVO" | head -5 | xargs cat -n 2>/dev/null | head -100Repository: YAPP-Github/Reed-Server
Length of output: 2831
🏁 Script executed:
#!/bin/bash
# Double-check request class to understand intent - is pageNumber meant to be optional?
cat -n apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequest.kt | head -50Repository: YAPP-Github/Reed-Server
Length of output: 2131
🏁 Script executed:
#!/bin/bash
# Verify: check if there's any version/migration strategy or deprecation plan
rg -n "version|deprecated|V1|migration" apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ --type kotlin | head -20Repository: YAPP-Github/Reed-Server
Length of output: 49
스키마 설명에 nullable 여부 명시 필요
pageNumber 필드의 스키마 설명이 nullable 동작을 명확히 하지 않습니다. ReadingRecordResponseV2와 일관성 있게 "(선택)" 또는 유사한 표현을 추가하여 선택 사항임을 명시하세요.
수정 예시: "현재 읽은 페이지 번호 (선택)"
🤖 Prompt for AI Agents
In
apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/ReadingRecordResponse.kt
around lines 20-21, the @Schema description for pageNumber does not indicate
that the field is nullable/optional; update the description text to match
ReadingRecordResponseV2 by appending an indicator such as " (선택)" so it reads
e.g. "현재 읽은 페이지 번호 (선택)"; ensure the example remains unchanged and keep the
annotation formatting consistent.
| val existingRecord = readingRecordDomainService.findById(readingRecordId) | ||
| val newPrimaryEmotion = request.primaryEmotion ?: existingRecord.primaryEmotion | ||
|
|
||
| // Validate detail emotion tags if provided | ||
| if (!request.detailEmotionTagIds.isNullOrEmpty()) { | ||
| validateDetailEmotionTags(request.detailEmotionTagIds, newPrimaryEmotion) | ||
| } |
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.
primaryEmotion 변경 시 기존 detailEmotions 불일치 가능성
primaryEmotion이 변경되었지만 detailEmotionTagIds가 제공되지 않은 경우, 기존 detail emotion 태그들이 새로운 primaryEmotion과 일치하지 않을 수 있습니다.
예: WARMTH로 생성된 기록의 detail tags가 있는 상태에서 primaryEmotion만 JOY로 변경하면, 기존 WARMTH의 detail tags가 그대로 남게 됩니다.
🔎 수정 제안
fun updateReadingRecord(
userId: UUID,
readingRecordId: UUID,
request: UpdateReadingRecordRequestV2
): ReadingRecordResponseV2 {
val existingRecord = readingRecordDomainService.findById(readingRecordId)
val newPrimaryEmotion = request.primaryEmotion ?: existingRecord.primaryEmotion
+ val primaryEmotionChanged = request.primaryEmotion != null && request.primaryEmotion != existingRecord.primaryEmotion
// Validate detail emotion tags if provided
if (!request.detailEmotionTagIds.isNullOrEmpty()) {
validateDetailEmotionTags(request.detailEmotionTagIds, newPrimaryEmotion)
}
+
+ // If primary emotion changed but no new detail tags provided, clear existing ones
+ if (primaryEmotionChanged && request.detailEmotionTagIds == null) {
+ readingRecordDetailTagDomainService.deleteAllByReadingRecordId(readingRecordId)
+ }🤖 Prompt for AI Agents
In
apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordServiceV2.kt
around lines 109-115, if primaryEmotion is changed but
request.detailEmotionTagIds is null/empty the existing record's
detailEmotionTags may not match the new primaryEmotion; detect when
request.primaryEmotion != existingRecord.primaryEmotion and
request.detailEmotionTagIds is null or empty, then validate the
existingRecord.detailEmotionTagIds against the new primaryEmotion and reject the
update with a clear validation error (or require the client to provide new
matching detailEmotionTagIds) if any existing detail tags are incompatible;
update the service to perform this validation before persisting so mismatched
detail tags are not left behind.
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.
@minwoo1999, CodeRabbit이 지적한 부분 확인했습니다. 유효한 이슈입니다. 👍
문제 상황
primaryEmotion만 변경하고 detailEmotionTagIds를 제공하지 않으면, 기존 세부 감정 태그들이 새로운 대분류 감정과 불일치하게 됩니다.
예시:
- 독서 기록 생성: primaryEmotion = WARMTH, detailEmotionTagIds = [따뜻한, 포근한]
- 업데이트 요청: primaryEmotion = JOY, detailEmotionTagIds = null
- 결과: primaryEmotion은 JOY지만, detail tags는 여전히 WARMTH 카테고리의 태그들이 남아있음 ❌
권장 해결 방안
primaryEmotion이 변경되었는데 새로운 detailEmotionTagIds가 제공되지 않은 경우, 기존 detail tags를 자동으로 삭제하는 것을 추천합니다:
fun updateReadingRecord(
userId: UUID,
readingRecordId: UUID,
request: UpdateReadingRecordRequestV2
): ReadingRecordResponseV2 {
val existingRecord = readingRecordDomainService.findById(readingRecordId)
val newPrimaryEmotion = request.primaryEmotion ?: existingRecord.primaryEmotion
val primaryEmotionChanged = request.primaryEmotion != null &&
request.primaryEmotion != existingRecord.primaryEmotion
// Validate detail emotion tags if provided
if (!request.detailEmotionTagIds.isNullOrEmpty()) {
validateDetailEmotionTags(request.detailEmotionTagIds, newPrimaryEmotion)
}
// Clear existing detail tags if primary emotion changed without new tags
if (primaryEmotionChanged && request.detailEmotionTagIds == null) {
readingRecordDetailTagDomainService.deleteAllByReadingRecordId(readingRecordId)
}
// ... rest of update logic
}대안:
- 클라이언트에게
primaryEmotion변경 시 반드시 새로운detailEmotionTagIds를 요구하도록 validation 추가 (더 엄격한 접근)
어떤 방식으로 처리하시겠습니까?
| fun updateReadingRecord( | ||
| userId: UUID, | ||
| readingRecordId: UUID, | ||
| request: UpdateReadingRecordRequestV2 | ||
| ): ReadingRecordResponseV2 { | ||
| userService.validateUserExists(userId) | ||
|
|
||
| return readingRecordServiceV2.updateReadingRecord( | ||
| userId = userId, | ||
| readingRecordId = readingRecordId, | ||
| request = request | ||
| ) | ||
| } |
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.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "*ReadingRecordServiceV2*"Repository: YAPP-Github/Reed-Server
Length of output: 151
🏁 Script executed:
cat -n ./apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordServiceV2.ktRepository: YAPP-Github/Reed-Server
Length of output: 8690
🏁 Script executed:
cat -n ./apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCaseV2.ktRepository: YAPP-Github/Reed-Server
Length of output: 3518
🏁 Script executed:
find . -type f -name "*ReadingRecordDomainService*"Repository: YAPP-Github/Reed-Server
Length of output: 151
🏁 Script executed:
cat -n ./domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.ktRepository: YAPP-Github/Reed-Server
Length of output: 13488
독서 기록 접근 및 수정 시 소유권 검증이 필수입니다.
getReadingRecordDetail, deleteReadingRecord, updateReadingRecord 메서드 모두에서 사용자의 읽기 기록에 대한 소유권 검증이 누락되어 있습니다. 현재는 userId만 검증하고 있어, 다른 사용자의 기록에 접근할 수 있는 보안 취약점이 있습니다. ReadingRecord와 연관된 UserBook의 소유자가 요청 사용자와 일치하는지 확인해야 합니다.
🤖 Prompt for AI Agents
In
apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCaseV2.kt
around lines 67 to 79, the updateReadingRecord method validates only that the
user exists but does not verify ownership of the target ReadingRecord/UserBook;
add a check that the ReadingRecord (or its associated UserBook) belongs to the
requesting userId before calling readingRecordServiceV2.updateReadingRecord.
Implement this by fetching the ReadingRecord (or UserBook) via the appropriate
repository/service, compare its owner userId to the supplied userId, and if they
differ throw a suitable access denied/not found exception; only proceed to call
readingRecordServiceV2.updateReadingRecord when ownership is confirmed.
| fun deleteReadingRecord( | ||
| userId: UUID, | ||
| readingRecordId: UUID | ||
| ) { | ||
| userService.validateUserExists(userId) | ||
| readingRecordServiceV2.deleteReadingRecord(readingRecordId) | ||
| } |
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.
독서 기록 삭제 시 소유권 검증을 고려하세요.
deleteReadingRecord 메서드는 사용자 존재 여부만 확인하고, 해당 독서 기록이 실제로 요청한 사용자의 것인지는 검증하지 않습니다. 다른 사용자의 독서 기록을 삭제할 수 있는 보안 취약점이 될 수 있습니다.
🔎 소유권 검증 추가 제안
@Transactional
fun deleteReadingRecord(
userId: UUID,
readingRecordId: UUID
) {
userService.validateUserExists(userId)
- readingRecordServiceV2.deleteReadingRecord(readingRecordId)
+ // userId를 함께 전달하여 소유권 검증
+ readingRecordServiceV2.deleteReadingRecord(userId, readingRecordId)
}Committable suggestion skipped: line range outside the PR's diff.
| fun findAllById(ids: List<UUID>): List<DetailTag> { | ||
| if (ids.isEmpty()) return emptyList() | ||
| return detailTagRepository.findAllById(ids) | ||
| } |
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 | 🔵 Trivial
선택적 제안: 요청된 ID의 존재 여부 검증 고려
findAllById가 부분 결과를 반환할 수 있습니다 (일부 ID가 존재하지 않는 경우). 비즈니스 요구사항에 따라 존재하지 않는 ID에 대한 명시적 검증이나 예외 처리가 필요할 수 있습니다.
🔎 ID 검증을 추가하는 경우의 예시
fun findAllById(ids: List<UUID>): List<DetailTag> {
if (ids.isEmpty()) return emptyList()
- return detailTagRepository.findAllById(ids)
+ val found = detailTagRepository.findAllById(ids)
+ if (found.size != ids.size) {
+ val foundIds = found.map { it.id.value }.toSet()
+ val missingIds = ids.filterNot { it in foundIds }
+ throw DetailTagNotFoundException("DetailTags not found: $missingIds")
+ }
+ return found
}Committable suggestion skipped: line range outside the PR's diff.
| fun deleteAllByUserBookId(userBookId: UUID) { | ||
| readingRecordRepository.deleteAllByUserBookId(userBookId) | ||
| } |
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.
🧩 Analysis chain
🏁 Script executed:
rg -n "deleteAllByUserBookId" --type kotlinRepository: YAPP-Github/Reed-Server
Length of output: 1377
🏁 Script executed:
# Also search for userBook deletion patterns
rg -n "deleteUserBook|delete.*userBook" --type kotlin -iRepository: YAPP-Github/Reed-Server
Length of output: 2372
🏁 Script executed:
# Check the ReadingRecordRepository implementation
rg -n "class ReadingRecordRepository|fun deleteAllByUserBookId" --type kotlin -A 5Repository: YAPP-Github/Reed-Server
Length of output: 4816
🏁 Script executed:
# Look for userBook.readingRecordCount updates in the service
rg -n "readingRecordCount" --type kotlinRepository: YAPP-Github/Reed-Server
Length of output: 1715
deleteAllByUserBookId에서 UserBook의 readingRecordCount 업데이트 누락
이 메서드는 특정 UserBook과 연결된 모든 읽기 기록을 삭제하지만, UserBook의 readingRecordCount를 업데이트하지 않습니다. BookUseCase에서 UserBook 삭제 전에 호출되지만, 이 메서드가 ReadingRecordService를 통해 공개적으로 노출되어 있어 독립적으로 호출될 수 있으므로 데이터 불일치가 발생할 수 있습니다. decreaseReadingRecordCount() 호출을 추가하여 일관성을 보장해야 합니다.
🤖 Prompt for AI Agents
In
domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt
around lines 277 to 279, the deleteAllByUserBookId method removes all reading
records but does not update the associated UserBook.readingRecordCount; modify
the method to first determine how many records will be/were deleted (e.g.,
countByUserBookId or capture deletion result), then call the existing
decreaseReadingRecordCount(userBookId, count) on the appropriate UserBook
service (or call it repeatedly if only a single-decrement API exists) and ensure
the whole operation runs in the same transaction so the UserBook count stays
consistent.
| -- 3. reading_record_detail_tags 테이블 생성 | ||
| CREATE TABLE reading_record_detail_tags ( | ||
| id VARCHAR(36) NOT NULL, | ||
| created_at datetime(6) NOT NULL, | ||
| updated_at datetime(6) NOT NULL, | ||
| deleted_at datetime(6) NULL, | ||
| reading_record_id VARCHAR(36) NOT NULL, | ||
| detail_tag_id VARCHAR(36) NOT NULL, | ||
| CONSTRAINT pk_reading_record_detail_tags PRIMARY KEY (id), | ||
| CONSTRAINT uq_record_detail_tag UNIQUE (reading_record_id, detail_tag_id), | ||
| CONSTRAINT fk_rrdt_reading_record FOREIGN KEY (reading_record_id) REFERENCES reading_records(id) ON DELETE CASCADE, | ||
| CONSTRAINT fk_rrdt_detail_tag FOREIGN KEY (detail_tag_id) REFERENCES detail_tags(id) | ||
| ); | ||
|
|
||
| CREATE INDEX idx_rrdt_reading_record_id ON reading_record_detail_tags(reading_record_id); | ||
| CREATE INDEX idx_rrdt_detail_tag_id ON reading_record_detail_tags(detail_tag_id); |
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 | 🔵 Trivial
soft delete 쿼리 성능을 위한 인덱스 고려
reading_record_detail_tags 테이블에 deleted_at 컬럼이 있어 soft delete 패턴을 사용하는 것으로 보입니다. deleted_at IS NULL 조건으로 자주 조회한다면, 복합 인덱스 추가를 고려해 주세요:
CREATE INDEX idx_rrdt_record_deleted ON reading_record_detail_tags(reading_record_id, deleted_at);🤖 Prompt for AI Agents
In
infra/src/main/resources/db/migration/mysql/V20251224_001__add_primary_emotion_and_detail_tags.sql
around lines 40 to 55, add a composite index to support queries that filter by
reading_record_id and deleted_at; create an index on (reading_record_id,
deleted_at) (and optionally one on (detail_tag_id, deleted_at) if those queries
exist) to be added after the table creation and before the end of the migration.
|
minwoo1999
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.
고생하셨습니다 :)
| val existingRecord = readingRecordDomainService.findById(readingRecordId) | ||
| val newPrimaryEmotion = request.primaryEmotion ?: existingRecord.primaryEmotion | ||
|
|
||
| // Validate detail emotion tags if provided | ||
| if (!request.detailEmotionTagIds.isNullOrEmpty()) { | ||
| validateDetailEmotionTags(request.detailEmotionTagIds, newPrimaryEmotion) | ||
| } |
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 | ||
|
|
||
| @Schema(name = "EmotionListResponse", description = "감정 목록 응답") | ||
| data class EmotionListResponse( |
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.
private construct 안붙어있는 dto클래스들이 몇몇개 보여서 확인 부탁드릴게요 !!


🔗 관련 이슈
📘 작업 유형
📙 작업 내역
감정 시스템 V2 구조
Domain Layer
primaryEmotion필드 추가,pageNumbernullable 변경Infra Layer
V20251224_001)detail_tags테이블 생성reading_record_detail_tags테이블 생성reading_records.primary_emotion컬럼 추가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- 감정 목록 조회 APIV1 호환성
pageNumbernullable 변경🧪 테스트 내역
✅ PR 체크리스트
💬 추가 설명 or 리뷰 포인트
ApplicationService가 여러 DomainService를 조합하는 구조로 설계했습니다Summary by CodeRabbit
릴리스 노트
New Features
Enhancements
✏️ Tip: You can customize this high-level summary in your review settings.