-
Notifications
You must be signed in to change notification settings - Fork 1
feat: 독서 기록 V2 API 및 대분류/세부 감정 시스템 구현 #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
Conversation
Walkthrough상세 감정 태그 도메인·JPA·리포지토리, 감정 목록 API(v2), 독서기록 V2(생성/조회/수정/삭제) 서비스·유스케이스 및 DB 마이그레이션을 추가해 읽기 기록과 상세 감정 태그 연관을 지원합니다. Changes
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)
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro 📒 Files selected for processing (7)
⏰ 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)
🔇 Additional comments (19)
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 모두 새 필드를 적절히 처리합니다.
apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerApiV2.kt
Outdated
Show resolved
Hide resolved
apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequestV2.kt
Show resolved
Hide resolved
apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequestV2.kt
Show resolved
Hide resolved
apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/response/ReadingRecordResponse.kt
Outdated
Show resolved
Hide resolved
apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordServiceV2.kt
Show resolved
Hide resolved
apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCaseV2.kt
Show resolved
Hide resolved
apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCaseV2.kt
Show resolved
Hide resolved
domain/src/main/kotlin/org/yapp/domain/detailtag/DetailTagDomainService.kt
Show resolved
Hide resolved
domain/src/main/kotlin/org/yapp/domain/readingrecord/ReadingRecordDomainService.kt
Show resolved
Hide resolved
...src/main/resources/db/migration/mysql/V20251224_001__add_primary_emotion_and_detail_tags.sql
Show resolved
Hide resolved
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.
고생하셨습니다 :)
apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordServiceV2.kt
Show resolved
Hide resolved
apis/src/main/kotlin/org/yapp/apis/emotion/dto/response/EmotionListResponse.kt
Outdated
Show resolved
Hide resolved
변경 사항: - primaryEmotion 변경 + 새 detailTags 제공 → 새 태그로 교체 - primaryEmotion 변경 + detailTags 미제공 → 기존 태그 삭제 (불일치 방지) - primaryEmotion 유지 + 새 detailTags 제공 → 새 태그로 교체 - primaryEmotion 유지 + detailTags 미제공 → 기존 태그 유지 리팩토링: - updateDetailEmotionTags() private 메서드로 분리 - isPrimaryEmotionChanged() 헬퍼 메서드 추가
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
♻️ 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
📒 Files selected for processing (12)
apis/src/main/kotlin/org/yapp/apis/book/exception/UserBookErrorCode.ktapis/src/main/kotlin/org/yapp/apis/book/service/UserBookService.ktapis/src/main/kotlin/org/yapp/apis/emotion/dto/response/EmotionListResponse.ktapis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerApiV2.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/ReadingRecordUseCase.ktapis/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()메서드는 코드베이스에 존재하지 않으므로 "제거되었다"는 전제가 올바르지 않습니다. 감정 태그 검증은 이미 다른 계층에서 수행되고 있습니다:
- DTO 계층:
CreateReadingRecordRequest의emotionTags필드는@Size(max = 1)(리스트 크기)와@Size(max = 10)(각 요소 길이) 검증 어노테이션이 적용되어 있습니다.- 도메인 계층:
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로 설계되어 부분 업데이트를 올바르게 지원하고 있습니다.
detailEmotionTagIds가null이면 변경하지 않는다는 의미가 스키마 설명에 명확히 문서화되어 있어 좋습니다.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로의 변환이 올바르게 구현되었습니다.primaryEmotion과detailEmotions를 각각의 중첩 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 구현이 우수합니다.독서 기록 생성 로직이 잘 구조화되어 있습니다:
- 세부 감정 태그 유효성 검증
- 독서 기록 생성
- 세부 감정 태그 연관 저장
- 사용자 lastActivity 업데이트
- 응답 생성
각 단계가 명확하게 분리되어 있고, 트랜잭션 일관성이 보장됩니다.
68-102: 페이징 조회 시 N+1 문제를 효과적으로 방지했습니다.
getReadingRecordsByDynamicCondition메서드가 다음과 같이 최적화되었습니다:
- 모든 독서 기록 ID를 한 번에 수집
findByReadingRecordIdIn으로 일괄 조회- detail tag도 일괄 조회 후 메모리에서 그룹화
이는 N+1 쿼리 문제를 방지하는 효율적인 구현입니다.
apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerApiV2.kt
Show resolved
Hide resolved
apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerApiV2.kt
Show resolved
Hide resolved
apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerApiV2.kt
Outdated
Show resolved
Hide resolved
apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequestV2.kt
Outdated
Show resolved
Hide resolved
apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/UpdateReadingRecordRequestV2.kt
Show resolved
Hide resolved
apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordService.kt
Show resolved
Hide resolved
apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordServiceV2.kt
Show resolved
Hide resolved
apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCase.kt
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: 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
📒 Files selected for processing (4)
apis/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/CreateReadingRecordRequestV2.ktapis/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!세 가지 케이스를 명확하게 처리합니다:
- 새 태그 제공 시 → 기존 삭제 후 새 태그 저장
- primaryEmotion만 변경 시 → 기존 태그 삭제
- 그 외 → 기존 태그 유지
apis/src/main/kotlin/org/yapp/apis/readingrecord/dto/request/CreateReadingRecordRequestV2.kt (2)
25-46: LGTM!필드 검증이 적절하게 구성되었습니다.
@NotNull이primaryEmotion에 추가되어 이전 리뷰 피드백이 반영되었습니다.
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를 적절하게 반환합니다.
apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerApiV2.kt
Show resolved
Hide resolved
apis/src/main/kotlin/org/yapp/apis/readingrecord/controller/ReadingRecordControllerV2.kt
Show resolved
Hide resolved
apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordServiceV2.kt
Show resolved
Hide resolved
apis/src/main/kotlin/org/yapp/apis/readingrecord/service/ReadingRecordServiceV2.kt
Show resolved
Hide resolved
|


🔗 관련 이슈
📘 작업 유형
📙 작업 내역
감정 시스템 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- 감정 목록 조회 APIDTO 리팩토링
primaryEmotion 변경 시 데이터 일관성 보장
소유권 검증 강화 (V1/V2 공통)
API 문서 개선
V1 호환성
🧪 테스트 내역
✅ PR 체크리스트
💬 추가 설명 or 리뷰 포인트
ApplicationService가 여러 DomainService를 조합하는 구조로 설계했습니다Summary by CodeRabbit
New Features
Enhancements
Bug Fixes / Changes
✏️ Tip: You can customize this high-level summary in your review settings.