Conversation
|
""" Walkthrough기록(Record) 생성 API를 도입하는 대규모 변경입니다. 컨트롤러, 서비스, 포트, 어댑터, 도메인, DTO 및 검증 로직이 추가되었으며, 불필요한 더미 파일들은 삭제되었습니다. 도메인 레벨의 유효성 검사와 예외 코드가 신설되었고, 통합 및 단위 테스트도 함께 추가되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Controller as RecordCommandController
participant Service as RecordCreateService
participant RoomPort
participant BookPort
participant RecordPort
participant Persistence as RecordCommandPersistenceAdapter
participant UserRepo
participant RoomRepo
Client->>Controller: POST /rooms/{roomId}/record (RecordCreateRequest)
Controller->>Service: createRecord(RecordCreateCommand)
Service->>RoomPort: getRoom(command.roomId)
RoomPort-->>Service: Room
Service->>BookPort: getBook(room.bookId)
BookPort-->>Service: Book
Service->>Service: Record.validatePage(book.totalPageCount)\nRecord.validateOverview(book.totalPageCount)
Service->>RecordPort: saveRecord(Record)
RecordPort->>Persistence: saveRecord(Record)
Persistence->>UserRepo: findById(record.creatorId)
Persistence->>RoomRepo: findById(record.roomId)
Persistence->>Persistence: RecordMapper.toEntity(…)
Persistence->>Persistence: recordJpaRepository.save(…)
Persistence-->>RecordPort: recordId
RecordPort-->>Service: recordId
Service-->>Controller: recordId
Controller-->>Client: BaseResponse<RecordCreateResponse>
Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Suggested labels
Poem
""" 📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
✨ Finishing Touches
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (4)
src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java (1)
23-24: 어노테이션 순서 일관성 제안대부분의 테스트 클래스에서는
@Import뒤에@DisplayName이 위치합니다. 해당 파일만 순서가 반대라 소스 일관성이 깨집니다.-@DisplayName("[JPA] FeedJpaEntity 테스트") -@Import(konkuk.thip.config.TestQuerydslConfig.class) // DataJpaTest 이므로 JPA 제외 빈 추가로 import +@Import(konkuk.thip.config.TestQuerydslConfig.class) // DataJpaTest 이므로 JPA 제외 빈 추가로 import +@DisplayName("[JPA] FeedJpaEntity 테스트")src/main/java/konkuk/thip/record/adapter/out/persistence/RecordCommandPersistenceAdapter.java (1)
27-27: 입력 파라미터 검증 추가를 고려해보세요record 파라미터에 대한 null 체크를 추가하면 더 안전한 코드가 될 것 같습니다.
다음과 같이 가드 클로즈를 추가하는 것을 고려해보세요:
@Override public Long saveRecord(Record record) { + if (record == null) { + throw new IllegalArgumentException("Record cannot be null"); + } UserJpaEntity userJpaEntity = userJpaRepository.findById(record.getCreatorId()).orElseThrow(src/main/java/konkuk/thip/record/domain/Record.java (1)
43-64: 도메인 검증 메서드의 호출 시점을 명확히 해주세요.검증 메서드들이 잘 구현되어 있지만, 언제 호출되는지 명확하지 않습니다. 일반적으로 도메인 객체 생성 시점이나 상태 변경 시점에 자동으로 호출되도록 하는 것이 좋습니다.
다음과 같은 방법을 고려해보세요:
- 생성자나 빌더에서 자동 호출
- 별도의 validate() 메서드에서 모든 검증 로직을 한 번에 수행
- 도메인 서비스에서 명시적 호출 (현재 구현 방식으로 보임)
src/test/java/konkuk/thip/record/domain/RecordTest.java (1)
48-52: 경계값 테스트 케이스를 추가로 고려해보세요.현재 80% 정확히 일치하는 케이스(16/20)는 테스트되고 있지만, 80%를 약간 초과하는 케이스(예: 17/20 = 85%)도 테스트해보면 좋겠습니다.
다음과 같은 테스트 케이스를 추가할 수 있습니다:
@Test @DisplayName("validateOverview: 진행률 80% 초과이고 isOverview=true 이면, 예외가 발생하지 않는다.") void validate_overview_ratio_above_80_percent() { Record record = Record.withoutId("content", 1L, 17, true, 1L); // 17/20 = 0.85 assertDoesNotThrow(() -> record.validateOverview(20)); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (31)
src/main/java/konkuk/thip/common/exception/code/ErrorCode.java(1 hunks)src/main/java/konkuk/thip/record/adapter/in/web/RecordCommandController.java(1 hunks)src/main/java/konkuk/thip/record/adapter/in/web/request/DummyRequest.java(0 hunks)src/main/java/konkuk/thip/record/adapter/in/web/request/RecordCreateRequest.java(1 hunks)src/main/java/konkuk/thip/record/adapter/in/web/response/DummyResponse.java(0 hunks)src/main/java/konkuk/thip/record/adapter/in/web/response/RecordCreateResponse.java(1 hunks)src/main/java/konkuk/thip/record/adapter/out/persistence/RecordCommandPersistenceAdapter.java(1 hunks)src/main/java/konkuk/thip/record/application/port/in/DummyUseCase.java(0 hunks)src/main/java/konkuk/thip/record/application/port/in/RecordCreateUseCase.java(1 hunks)src/main/java/konkuk/thip/record/application/port/in/dto/DummyCommand.java(0 hunks)src/main/java/konkuk/thip/record/application/port/in/dto/RecordCreateCommand.java(1 hunks)src/main/java/konkuk/thip/record/application/port/out/RecordCommandPort.java(1 hunks)src/main/java/konkuk/thip/record/application/service/RecordCreateService.java(1 hunks)src/main/java/konkuk/thip/record/application/service/RecordService.java(0 hunks)src/main/java/konkuk/thip/record/domain/Record.java(2 hunks)src/test/java/konkuk/thip/book/adapter/in/web/BookChangeSavedControllerTest.java(1 hunks)src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchControllerTest.java(1 hunks)src/test/java/konkuk/thip/book/adapter/in/web/BookQueryControllerTest.java(1 hunks)src/test/java/konkuk/thip/book/adapter/out/api/NaverApiUtilTest.java(1 hunks)src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java(1 hunks)src/test/java/konkuk/thip/record/adapter/in/web/RecordCreateControllerTest.java(1 hunks)src/test/java/konkuk/thip/record/domain/RecordTest.java(1 hunks)src/test/java/konkuk/thip/room/adapter/out/jpa/RecordJpaEntityTest.java(1 hunks)src/test/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntityTest.java(1 hunks)src/test/java/konkuk/thip/room/adapter/out/jpa/VoteJpaEntityTest.java(1 hunks)src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java(1 hunks)src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java(1 hunks)src/test/java/konkuk/thip/user/adapter/in/web/UserViewAliasChoiceControllerTest.java(1 hunks)src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java(1 hunks)src/test/java/konkuk/thip/vote/adapter/in/web/VoteCreateControllerTest.java(1 hunks)src/test/java/konkuk/thip/vote/domain/VoteTest.java(1 hunks)
💤 Files with no reviewable changes (5)
- src/main/java/konkuk/thip/record/application/port/in/DummyUseCase.java
- src/main/java/konkuk/thip/record/application/port/in/dto/DummyCommand.java
- src/main/java/konkuk/thip/record/adapter/in/web/response/DummyResponse.java
- src/main/java/konkuk/thip/record/adapter/in/web/request/DummyRequest.java
- src/main/java/konkuk/thip/record/application/service/RecordService.java
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/test/java/konkuk/thip/room/adapter/out/jpa/RecordJpaEntityTest.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#36
File: src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java:7-7
Timestamp: 2025-06-29T09:47:31.299Z
Learning: Spring Data JPA에서 findBy{FieldName} 패턴의 메서드는 명시적 선언 없이 자동으로 생성되며, Optional<Entity> 반환 타입을 사용하는 것이 null 안전성을 위해 권장됩니다.
src/test/java/konkuk/thip/room/adapter/out/jpa/VoteJpaEntityTest.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#36
File: src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java:7-7
Timestamp: 2025-06-29T09:47:31.299Z
Learning: Spring Data JPA에서 findBy{FieldName} 패턴의 메서드는 명시적 선언 없이 자동으로 생성되며, Optional<Entity> 반환 타입을 사용하는 것이 null 안전성을 위해 권장됩니다.
src/main/java/konkuk/thip/record/application/port/out/RecordCommandPort.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#36
File: src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java:7-7
Timestamp: 2025-06-29T09:47:31.299Z
Learning: Spring Data JPA에서 findBy{FieldName} 패턴의 메서드는 명시적 선언 없이 자동으로 생성되며, Optional<Entity> 반환 타입을 사용하는 것이 null 안전성을 위해 권장됩니다.
src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#36
File: src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java:7-7
Timestamp: 2025-06-29T09:47:31.299Z
Learning: Spring Data JPA에서 findBy{FieldName} 패턴의 메서드는 명시적 선언 없이 자동으로 생성되며, Optional<Entity> 반환 타입을 사용하는 것이 null 안전성을 위해 권장됩니다.
src/main/java/konkuk/thip/record/application/port/in/RecordCreateUseCase.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
🧬 Code Graph Analysis (6)
src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java (2)
src/test/java/konkuk/thip/book/adapter/out/api/NaverApiUtilTest.java (1)
DisplayName(13-71)src/test/java/konkuk/thip/vote/domain/VoteTest.java (1)
DisplayName(9-63)
src/test/java/konkuk/thip/room/adapter/out/jpa/VoteJpaEntityTest.java (1)
src/test/java/konkuk/thip/vote/domain/VoteTest.java (1)
DisplayName(9-63)
src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchControllerTest.java (2)
src/test/java/konkuk/thip/book/adapter/out/api/NaverApiUtilTest.java (1)
DisplayName(13-71)src/test/java/konkuk/thip/vote/domain/VoteTest.java (1)
DisplayName(9-63)
src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java (2)
src/test/java/konkuk/thip/book/adapter/out/api/NaverApiUtilTest.java (1)
DisplayName(13-71)src/test/java/konkuk/thip/vote/domain/VoteTest.java (1)
DisplayName(9-63)
src/test/java/konkuk/thip/book/adapter/in/web/BookQueryControllerTest.java (2)
src/test/java/konkuk/thip/book/adapter/out/api/NaverApiUtilTest.java (1)
DisplayName(13-71)src/test/java/konkuk/thip/vote/domain/VoteTest.java (1)
DisplayName(9-63)
src/test/java/konkuk/thip/record/adapter/in/web/RecordCreateControllerTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
TestEntityFactory(13-75)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: build
🔇 Additional comments (30)
src/test/java/konkuk/thip/vote/domain/VoteTest.java (1)
9-9: 테스트 가독성 향상을 위한 DisplayName 추가 👍클래스 레벨
@DisplayName이 추가되어 JUnit 리포트에서 테스트 목적이 한눈에 들어옵니다. 팀 내 다른 테스트 클래스들과도 일관성을 유지하고 있어 좋습니다.src/test/java/konkuk/thip/vote/adapter/in/web/VoteCreateControllerTest.java (1)
47-47: 클래스 레벨 DisplayName 추가로 통합 테스트 리포트 가독성 향상
[통합] VoteCreateController 테스트라는 구체적 제목 덕분에 CI 결과를 빠르게 파악할 수 있습니다. 동일한 패턴을 전 프로젝트에 적용한 점도 👍src/test/java/konkuk/thip/book/adapter/out/api/NaverApiUtilTest.java (1)
13-14: ✅ 클래스 레벨@DisplayName추가, 테스트 가독성 향상 확인
테스트 실행 리포트에서 한눈에 용도를 파악할 수 있어 좋습니다. 별다른 문제 없습니다.src/test/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntityTest.java (1)
24-24:@DisplayName추가 👍가독성 향상을 위한 네이밍 주석 추가, 다른 클래스와도 일관적입니다.
src/test/java/konkuk/thip/room/adapter/out/jpa/VoteJpaEntityTest.java (1)
27-27:@DisplayName추가 👍테스트 리포트 가독성 향상에 도움 됩니다.
src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java (1)
20-20:@DisplayName추가 👍다른 JPA 엔티티 테스트와 동일한 패턴으로 일관성 유지되었습니다.
src/test/java/konkuk/thip/room/adapter/out/jpa/RecordJpaEntityTest.java (1)
27-27:@DisplayName추가 👍테스트 명세 가독성 향상에 기여합니다.
src/test/java/konkuk/thip/book/adapter/in/web/BookChangeSavedControllerTest.java (1)
38-38: DisplayName 추가로 테스트 가독성 향상
클래스 레벨@DisplayName이 추가되어 통합 테스트 결과를 식별하기 쉬워졌습니다. 네이밍 규칙도 일관적입니다.src/test/java/konkuk/thip/book/adapter/in/web/BookQueryControllerTest.java (1)
32-32: DisplayName 적용 👍
테스트 클래스에 의미 있는 이름이 부여되어 리포트 해석이 간편해졌습니다.src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchControllerTest.java (1)
34-34: DisplayName 일관성 확보
다른 통합 테스트들과 동일한 형식의@DisplayName을 적용하여 가독성과 일관성이 향상되었습니다.src/test/java/konkuk/thip/user/adapter/in/web/UserViewAliasChoiceControllerTest.java (1)
32-32: 테스트 식별자 명확화
@DisplayName추가로 테스트 목적이 한눈에 파악됩니다. 변경 사항 문제 없습니다.src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java (1)
32-32: DisplayName 추가 확인
통합 테스트 명칭을 명시해 리포트 가독성이 개선되었습니다.src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java (1)
33-33: 테스트 클래스 표시명 추가 승인통합 테스트 클래스에
@DisplayName어노테이션을 추가하여 테스트 실행 시 가독성을 개선했습니다. 다른 테스트 클래스들과 일관성을 유지하는 좋은 변경입니다.src/main/java/konkuk/thip/record/application/port/out/RecordCommandPort.java (1)
8-8: CommandPort 메서드 추가 승인
saveRecord메서드가 프로젝트의 CQRS 패턴 컨벤션을 잘 따르고 있습니다. 도메인 객체를 받아 저장하고 ID를 반환하는 구조가 적절합니다.src/main/java/konkuk/thip/record/application/port/in/dto/RecordCreateCommand.java (1)
3-15: Command DTO 구조 승인
RecordCreateCommand레코드가 기록 생성에 필요한 모든 필드를 포함하고 있으며, 불변 객체로 잘 설계되었습니다. 헥사고날 아키텍처의 Command 패턴을 적절히 구현했습니다.src/main/java/konkuk/thip/record/application/port/in/RecordCreateUseCase.java (1)
5-9: UseCase 인터페이스 설계 승인
RecordCreateUseCase인터페이스가 단일 책임 원칙을 잘 따르고 있으며, 메서드 시그니처가 적절합니다. 헥사고날 아키텍처의 포트 인터페이스 패턴을 올바르게 구현했습니다.src/main/java/konkuk/thip/record/adapter/in/web/response/RecordCreateResponse.java (1)
3-9: Response DTO 구조 승인
RecordCreateResponse레코드가 깔끔하게 설계되었습니다. 정적 팩토리 메서드of()를 제공하여 객체 생성을 명확하게 표현했습니다. API 응답 구조가 일관성 있고 이해하기 쉽습니다.src/main/java/konkuk/thip/record/application/service/RecordCreateService.java (2)
15-37: 서비스 구조가 잘 설계되어 있습니다!CQRS 패턴을 적절히 따르고 있으며, 도메인 검증 로직이 별도 메소드로 잘 분리되어 있습니다. 트랜잭션 처리와 의존성 주입도 적절합니다.
39-48: 예외 처리 구현 확인 완료
RoomCommandPersistenceAdapter.findById(src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java:21–24)와
BookCommandPersistenceAdapter.findById(src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java:35–38)에서
.orElseThrow(() -> new EntityNotFoundException(...))를 사용해EntityNotFoundException을 던지도록 구현되어 있으므로, 추가적인 예외 처리 구현은 필요하지 않습니다.src/main/java/konkuk/thip/record/adapter/out/persistence/RecordCommandPersistenceAdapter.java (1)
26-39: saveRecord 메소드 구현이 적절합니다!EntityNotFoundException을 통한 예외 처리가 잘 구현되어 있고, RecordMapper를 활용한 도메인-JPA 엔티티 변환도 적절합니다.
src/main/java/konkuk/thip/record/adapter/in/web/request/RecordCreateRequest.java (1)
8-28: 요청 DTO가 잘 구현되어 있습니다!Jakarta Bean Validation 어노테이션이 적절히 적용되어 있고, 한국어 메시지도 일관성 있게 작성되어 있습니다. toCommand 메소드를 통한 변환 로직도 깔끔합니다.
src/main/java/konkuk/thip/record/adapter/in/web/RecordCommandController.java (2)
17-26: 컨트롤러 구현이 적절합니다!어노테이션 사용이 적절하고, BaseResponse를 통한 일관된 응답 구조도 좋습니다. 요청 변환 로직도 깔끔하게 구현되어 있습니다.
12-14: 클래스 레벨 @RequestMapping 불필요 확인
다른 대부분의 컨트롤러는 클래스 레벨에 @RequestMapping을 사용하지 않고 있어 현재 구현과 일관됩니다. TestExceptionController만 예외적으로 사용 중이므로, RecordCommandController에 별도 @RequestMapping을 추가할 필요가 없습니다.Likely an incorrect or invalid review comment.
src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (1)
75-77: RECORD 관련 에러 코드가 적절히 추가되었습니다!기존 VOTE 에러 코드와 일관된 패턴을 따르고 있으며, HTTP 상태 코드와 에러 메시지도 적절합니다. 120000번대 사용도 올바른 선택입니다.
src/main/java/konkuk/thip/record/domain/Record.java (3)
26-40: 정적 팩토리 메서드 구현이 적절합니다.ID 없이 Record 인스턴스를 생성하는 명확한 의도가 드러나는 좋은 구현입니다.
43-52: 총평 검증 로직이 올바르게 구현되었습니다.80% 이상 진행률 조건과 예외 메시지가 명확하게 구현되어 있습니다.
54-64: 페이지 범위 검증 로직이 올바르게 구현되었습니다.1부터 totalPageCount까지의 유효한 범위를 적절히 검증하고 있습니다.
src/test/java/konkuk/thip/record/domain/RecordTest.java (1)
1-63: 도메인 검증 로직에 대한 포괄적인 테스트가 잘 작성되었습니다.유효한 케이스와 예외 케이스를 모두 커버하고, 예외 타입과 메시지까지 검증하는 완전한 단위 테스트입니다.
src/test/java/konkuk/thip/record/adapter/in/web/RecordCreateControllerTest.java (2)
91-132: 기록 생성 API 통합 테스트가 잘 구현되었습니다.전체 플로우가 올바르게 테스트되고 있으며, 응답 데이터와 저장된 엔티티 모두 검증하고 있습니다.
69-77: 테스트 정리 로직이 적절하게 구현되었습니다.의존성 순서에 맞게 엔티티들을 정리하고 있어 테스트 격리가 잘 되어 있습니다.
| @Test | ||
| @DisplayName("[페이지 넘버, 총평 여부, 기록 내용]을 받아, 기록을 생성한다.") | ||
| void record_create_success() throws Exception { | ||
| //given | ||
| saveUserAndRoom(); | ||
|
|
||
| int page = 10; | ||
| boolean isOverview = false; | ||
| String content = "기록 내용"; | ||
|
|
||
| RecordCreateRequest request = new RecordCreateRequest( | ||
| page, | ||
| isOverview, | ||
| content | ||
| ); | ||
|
|
||
| Long userId = userJpaRepository.findAll().get(0).getUserId(); | ||
| Long roomId = roomJpaRepository.findAll().get(0).getRoomId(); | ||
|
|
||
| //when | ||
| ResultActions result = mockMvc.perform(post("/rooms/{roomId}/record", roomId) | ||
| .requestAttr("userId", userId) | ||
| .contentType(MediaType.APPLICATION_JSON) | ||
| .content(objectMapper.writeValueAsString(request) | ||
| )); | ||
|
|
||
| //then | ||
| result.andExpect(status().isOk()) | ||
| .andExpect(jsonPath("$.data.recordId").exists()); | ||
|
|
||
| String json = result.andReturn().getResponse().getContentAsString(); | ||
| JsonNode jsonNode = objectMapper.readTree(json); | ||
| Long recordId = jsonNode.path("data").path("recordId").asLong(); | ||
|
|
||
| RecordJpaEntity recordJpaEntity = recordJpaRepository.findById(recordId).orElse(null); | ||
|
|
||
| assertThat(recordJpaEntity).isNotNull(); | ||
| assertThat(recordJpaEntity.getUserJpaEntity().getUserId()).isEqualTo(userId); | ||
| assertThat(recordJpaEntity.getRoomJpaEntity().getRoomId()).isEqualTo(roomId); | ||
| assertThat(recordJpaEntity.getPage()).isEqualTo(page); | ||
| assertThat(recordJpaEntity.getContent()).isEqualTo(content); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("[page]가 누락되었을 때 400 Bad Request 반환") | ||
| void vote_create_page_null() throws Exception { | ||
| // given: page 누락 | ||
| Map<String, Object> request = Map.of( | ||
| "isOverview", false, | ||
| "content", "내용" | ||
| ); | ||
|
|
||
| // when & then | ||
| mockMvc.perform(post("/rooms/{roomId}/record", 1L) | ||
| .requestAttr("userId", 1L) | ||
| .contentType(MediaType.APPLICATION_JSON) | ||
| .content(objectMapper.writeValueAsString(request))) | ||
| .andExpect(status().isBadRequest()) | ||
| .andExpect(jsonPath("$.code").value(API_INVALID_PARAM.getCode())) | ||
| .andExpect(jsonPath("$.message", containsString("page는 필수입니다."))); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("[isOverview]가 누락되었을 때 400 Bad Request 반환") | ||
| void vote_create_is_over_view_null() throws Exception { | ||
| // given: isOverview 누락 | ||
| Map<String, Object> request = Map.of( | ||
| "page", 1, | ||
| "content", "내용" | ||
| ); | ||
|
|
||
| // when & then | ||
| mockMvc.perform(post("/rooms/{roomId}/record", 1L) | ||
| .requestAttr("userId", 1L) | ||
| .contentType(MediaType.APPLICATION_JSON) | ||
| .content(objectMapper.writeValueAsString(request))) | ||
| .andExpect(status().isBadRequest()) | ||
| .andExpect(jsonPath("$.code").value(API_INVALID_PARAM.getCode())) | ||
| .andExpect(jsonPath("$.message", containsString("isOverview(= 총평 여부)는 필수입니다."))); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("[content]가 빈 문자열일 때 400 Bad Request 반환") | ||
| void vote_create_content_blank() throws Exception { | ||
| // given | ||
| Map<String, Object> request = Map.of( | ||
| "page", 1, | ||
| "isOverview", false, | ||
| "content", "" | ||
| ); | ||
|
|
||
| // when & then | ||
| mockMvc.perform(post("/rooms/{roomId}/record", 1L) | ||
| .requestAttr("userId", 1L) | ||
| .contentType(MediaType.APPLICATION_JSON) | ||
| .content(objectMapper.writeValueAsString(request))) | ||
| .andExpect(status().isBadRequest()) | ||
| .andExpect(jsonPath("$.code").value(API_INVALID_PARAM.getCode())) | ||
| .andExpect(jsonPath("$.message", containsString("기록 내용은 필수입니다."))); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("[content]가 500자 초과일 때 400 Bad Request 반환") | ||
| void vote_create_content_too_long() throws Exception { | ||
| // given | ||
| String longContent = "가".repeat(501); | ||
| Map<String, Object> request = Map.of( | ||
| "page", 1, | ||
| "isOverview", false, | ||
| "content", longContent | ||
| ); | ||
|
|
||
| // when & then | ||
| mockMvc.perform(post("/rooms/{roomId}/record", 1L) | ||
| .requestAttr("userId", 1L) | ||
| .contentType(MediaType.APPLICATION_JSON) | ||
| .content(objectMapper.writeValueAsString(request))) | ||
| .andExpect(status().isBadRequest()) | ||
| .andExpect(jsonPath("$.code").value(API_INVALID_PARAM.getCode())) | ||
| .andExpect(jsonPath("$.message", containsString("기록 내용은 최대 500자 입니다."))); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
도메인 검증 로직에 대한 통합 테스트를 추가해주세요.
현재 API 레벨 검증은 잘 구현되어 있지만, 도메인 검증 로직(80% 규칙, 페이지 범위 검증)에 대한 통합 테스트가 누락되어 있습니다.
다음과 같은 테스트 케이스를 추가하는 것을 고려해보세요:
@Test
@DisplayName("진행률 80% 미만에서 총평 시도 시 도메인 예외 발생")
void record_create_overview_below_80_percent() throws Exception {
// TestEntityFactory.createBook()의 pageCount가 100이므로
// 79페이지는 79% 진행률
RecordCreateRequest request = new RecordCreateRequest(79, true, "내용");
// 도메인 예외 발생 확인
}
@Test
@DisplayName("페이지 범위 초과 시 도메인 예외 발생")
void record_create_page_out_of_range() throws Exception {
RecordCreateRequest request = new RecordCreateRequest(101, false, "내용");
// 도메인 예외 발생 확인
}🤖 Prompt for AI Agents
In
src/test/java/konkuk/thip/record/adapter/in/web/RecordCreateControllerTest.java
between lines 91 and 211, add integration tests for domain validation logic
missing in the current tests. Specifically, create tests that verify domain
exceptions are thrown when trying to create a summary record below 80% progress
and when the page number exceeds the valid range. Implement these tests by
constructing RecordCreateRequest instances with invalid page values (e.g., 79
for below 80% and 101 for out of range) and asserting that the domain exceptions
are properly triggered during the record creation API call.
src/test/java/konkuk/thip/record/adapter/in/web/RecordCreateControllerTest.java
Outdated
Show resolved
Hide resolved
src/test/java/konkuk/thip/record/adapter/in/web/RecordCreateControllerTest.java
Outdated
Show resolved
Hide resolved
src/test/java/konkuk/thip/record/adapter/in/web/RecordCreateControllerTest.java
Outdated
Show resolved
Hide resolved
src/test/java/konkuk/thip/record/adapter/in/web/RecordCreateControllerTest.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (7)
src/test/java/konkuk/thip/record/domain/RecordTest.java (3)
20-28: 예외 메시지 검증 방식을 개선하세요.예외 메시지 검증에서
assertTrue(ex.getCause().getMessage().contains(...))패턴은 취약합니다. 보다 구체적인 assertion을 사용하는 것이 좋습니다.- assertTrue(ex.getCause().getMessage().contains("현재 기록할 page = 0, 책 전체 page = 20")); + assertThat(ex.getCause().getMessage()).contains("현재 기록할 page = 0, 책 전체 page = 20");또는 예외 메시지를 완전히 매치하는 방식도 고려해보세요:
- assertTrue(ex.getCause().getMessage().contains("현재 기록할 page = 0, 책 전체 page = 20")); + assertThat(ex.getCause().getMessage()).isEqualTo("현재 기록할 page = 0, 책 전체 page = 20");
30-38: 예외 메시지 검증 방식을 개선하세요.이 테스트에서도 동일한 예외 메시지 검증 개선이 필요합니다.
- assertTrue(ex.getCause().getMessage().contains("현재 기록할 page = 25, 책 전체 page = 20")); + assertThat(ex.getCause().getMessage()).contains("현재 기록할 page = 25, 책 전체 page = 20");
54-62: 불필요한 주석을 제거하고 예외 메시지 검증을 개선하세요.라인 57의 주석
// 15/20 = 0.75은 테스트 로직과 관련이 없어 보입니다. 또한 예외 메시지 검증 방식도 개선이 필요합니다.- Record record = Record.withoutId("content", 1L, 15, true, 1L); // 15/20 = 0.75 + Record record = Record.withoutId("content", 1L, 15, true, 1L);- assertTrue(ex.getCause().getMessage().contains("현재 페이지 = 15")); + assertThat(ex.getCause().getMessage()).contains("현재 페이지 = 15");src/main/java/konkuk/thip/room/domain/Room.java (1)
40-42: 입력 검증 추가를 고려해보세요.roomPercentage 값에 대한 유효성 검증(예: 0-100 범위 체크)을 추가하는 것이 좋겠습니다.
public void updateRoomPercentage(double roomPercentage) { + if (roomPercentage < 0 || roomPercentage > 100) { + throw new IllegalArgumentException("Room percentage must be between 0 and 100"); + } this.roomPercentage = roomPercentage; }src/main/java/konkuk/thip/user/domain/UserRoom.java (1)
23-25: 하드코딩된 임계값을 상수로 분리하는 것을 고려해보세요.80% 임계값을 상수로 정의하면 유지보수성이 향상됩니다.
+ private static final double OVERVIEW_THRESHOLD = 80.0; + public boolean canWriteOverview() { - return userPercentage >= 80; + return userPercentage >= OVERVIEW_THRESHOLD; }src/main/java/konkuk/thip/record/application/service/RecordCreateService.java (1)
32-35: TODO 코멘트를 구체적인 이슈로 전환하는 것을 고려해보세요.roomPercentage 업데이트를 스케줄러로 분리하는 것은 성능과 책임 분리 측면에서 좋은 아이디어입니다.
이 TODO를 구체적인 이슈로 생성하여 추후 리팩토링 작업을 추적하시겠습니까?
src/test/java/konkuk/thip/user/domain/UserRoomTest.java (1)
15-16: 매직 넘버를 상수로 추출하는 것을 고려해보세요.80.0과 79.9 같은 임계값이 하드코딩되어 있습니다. 도메인 로직에서 사용하는 실제 상수값과 일치하는지 확인하고, 가능하다면 상수로 추출하여 테스트의 의도를 더 명확하게 표현할 수 있습니다.
+ private static final double OVERVIEW_THRESHOLD = 80.0; + private static final double BELOW_THRESHOLD = 79.9; + @Test @DisplayName("canWriteOverview: 사용자 퍼센트가 80 이상일 때, true를 반환한다.") void can_writeOverview() { //given UserRoom userRoom = UserRoom.builder() - .userPercentage(80.0) + .userPercentage(OVERVIEW_THRESHOLD) .build();Also applies to: 30-31
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
src/main/java/konkuk/thip/common/exception/code/ErrorCode.java(1 hunks)src/main/java/konkuk/thip/record/application/service/RecordCreateService.java(1 hunks)src/main/java/konkuk/thip/record/domain/Record.java(2 hunks)src/main/java/konkuk/thip/room/domain/Room.java(2 hunks)src/main/java/konkuk/thip/user/adapter/out/persistence/UserRoomCommandPersistenceAdapter.java(1 hunks)src/main/java/konkuk/thip/user/adapter/out/persistence/UserRoomJpaRepository.java(1 hunks)src/main/java/konkuk/thip/user/application/port/out/UserRoomCommandPort.java(1 hunks)src/main/java/konkuk/thip/user/application/port/out/UserRoomQueryPort.java(1 hunks)src/main/java/konkuk/thip/user/domain/UserRoom.java(1 hunks)src/main/java/konkuk/thip/vote/application/service/VoteCreateService.java(1 hunks)src/test/java/konkuk/thip/record/adapter/in/web/RecordCreateControllerTest.java(1 hunks)src/test/java/konkuk/thip/record/domain/RecordTest.java(1 hunks)src/test/java/konkuk/thip/user/domain/UserRoomTest.java(1 hunks)
✅ Files skipped from review due to trivial changes (2)
- src/main/java/konkuk/thip/vote/application/service/VoteCreateService.java
- src/main/java/konkuk/thip/user/application/port/out/UserRoomQueryPort.java
🚧 Files skipped from review as they are similar to previous changes (3)
- src/main/java/konkuk/thip/record/domain/Record.java
- src/test/java/konkuk/thip/record/adapter/in/web/RecordCreateControllerTest.java
- src/main/java/konkuk/thip/common/exception/code/ErrorCode.java
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/user/application/port/out/UserRoomCommandPort.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
src/main/java/konkuk/thip/user/adapter/out/persistence/UserRoomJpaRepository.java (2)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#36
File: src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java:7-7
Timestamp: 2025-06-29T09:47:31.299Z
Learning: Spring Data JPA에서 findBy{FieldName} 패턴의 메서드는 명시적 선언 없이 자동으로 생성되며, Optional<Entity> 반환 타입을 사용하는 것이 null 안전성을 위해 권장됩니다.
src/main/java/konkuk/thip/user/adapter/out/persistence/UserRoomCommandPersistenceAdapter.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
🧬 Code Graph Analysis (3)
src/main/java/konkuk/thip/user/adapter/out/persistence/UserRoomCommandPersistenceAdapter.java (1)
src/main/java/konkuk/thip/common/exception/EntityNotFoundException.java (1)
EntityNotFoundException(5-10)
src/test/java/konkuk/thip/record/domain/RecordTest.java (1)
src/test/java/konkuk/thip/user/domain/UserRoomTest.java (1)
DisplayName(8-84)
src/test/java/konkuk/thip/user/domain/UserRoomTest.java (1)
src/test/java/konkuk/thip/record/domain/RecordTest.java (1)
DisplayName(9-63)
🔇 Additional comments (17)
src/test/java/konkuk/thip/record/domain/RecordTest.java (4)
1-10: 테스트 클래스 구조가 잘 설계되었습니다.Record 도메인의 검증 로직에 대한 포괄적인 단위 테스트가 잘 구성되어 있습니다. 한국어 DisplayName 사용도 프로젝트 컨벤션에 맞습니다.
12-18: 유효한 페이지 범위 테스트 케이스가 적절합니다.경계값 테스트(1, 10)와 일반적인 케이스(20)를 모두 검증하여 충분한 테스트 커버리지를 제공합니다.
40-45: Overview 아닌 경우의 테스트가 올바릅니다.
isOverview=false일 때 검증이 통과하는 케이스를 정확히 테스트하고 있습니다.
47-52: Overview 정상 케이스 테스트가 적절합니다.페이지가 전체 페이지 수와 일치하는 경우의 정상 시나리오를 잘 검증하고 있습니다.
src/main/java/konkuk/thip/room/domain/Room.java (1)
36-38: 메서드 구현이 올바르고 명확합니다.도메인 로직이 간결하고 의도가 명확하게 드러납니다.
src/main/java/konkuk/thip/user/application/port/out/UserRoomCommandPort.java (1)
7-12: CQRS 패턴을 올바르게 구현했습니다.검색된 학습 내용에 따르면, CommandPort에 findByXXX 메서드를 통해 도메인 엔티티를 반환하는 것이 THIP 프로젝트의 컨벤션에 맞습니다.
src/main/java/konkuk/thip/user/adapter/out/persistence/UserRoomJpaRepository.java (1)
11-12: Spring Data JPA 메서드 네이밍과 Optional 사용이 적절합니다.검색된 학습 내용에 따르면, Optional 반환 타입 사용이 null 안전성을 위해 권장되며, 파생된 쿼리 메서드 네이밍도 올바릅니다.
src/main/java/konkuk/thip/record/application/service/RecordCreateService.java (2)
62-71: 방 진행률 계산 로직이 올바르게 구현되었습니다.사용자 진행률이 업데이트된 경우에만 전체 방 진행률을 재계산하는 조건부 로직이 효율적입니다.
84-98: 검증 메서드들이 잘 분리되어 있습니다.각 검증 로직이 별도 메서드로 분리되어 코드 가독성과 유지보수성이 좋습니다.
src/test/java/konkuk/thip/user/domain/UserRoomTest.java (4)
12-24: 개요 작성 권한 테스트 로직이 정확합니다.80% 임계값을 기준으로 한 개요 작성 권한 검증 로직이 올바르게 구현되어 있습니다.
26-39: 경계값 테스트가 적절합니다.79.9%로 경계값을 테스트하여 80% 미만에서 false를 반환하는 것을 확인하고 있습니다.
42-61: 사용자 진행률 업데이트 테스트가 포괄적입니다.요청 페이지가 현재 페이지보다 클 때의 업데이트 로직과 퍼센트 계산 로직을 정확히 검증하고 있습니다. 반환값과 상태 변경을 모두 확인하는 것이 좋습니다.
64-83: 역방향 케이스 테스트가 적절합니다.요청 페이지가 현재 페이지보다 작을 때 업데이트가 발생하지 않는다는 비즈니스 로직을 정확히 검증하고 있습니다.
src/main/java/konkuk/thip/user/adapter/out/persistence/UserRoomCommandPersistenceAdapter.java (4)
14-19: 의존성 주입과 어노테이션 구성이 적절합니다.
@Repository와@RequiredArgsConstructor를 사용한 의존성 주입이 Spring의 모범 사례를 따르고 있습니다.
21-28: 예외 처리가 적절하게 구현되어 있습니다.JPA 쿼리 메서드명이 길지만 연관 엔티티 탐색을 위해 필요한 구조입니다.
EntityNotFoundException을 사용한 예외 처리가 프로젝트의 예외 처리 패턴과 일치합니다.
30-35: 스트림을 활용한 변환 로직이 깔끔합니다.JPA 엔티티를 도메인 엔티티로 변환하는 과정에서 스트림과 메서드 레퍼런스를 적절히 사용하여 가독성이 좋습니다.
16-35: CQRS 패턴 준수를 확인했습니다.검색된 학습 내용에 따르면, THIP 프로젝트에서는 CommandPort에
findByXXX메서드를 통해 도메인 엔티티를 찾아오는 것이 컨벤션입니다. 이 어댑터가 해당 컨벤션을 정확히 따르고 있습니다.
src/main/java/konkuk/thip/record/application/service/RecordCreateService.java
Show resolved
Hide resolved
hd0rable
left a comment
There was a problem hiding this comment.
구현하느라 수고하셨습니다~~ 전체적으로 코드 너무 깔끔해서 보기 좋은것같아요!! 진짜 사소한 리뷰하나 남겼습니다 ㅎ
src/main/java/konkuk/thip/record/adapter/in/web/RecordCommandController.java
Outdated
Show resolved
Hide resolved
| @SpringBootTest | ||
| @AutoConfigureMockMvc | ||
| @ActiveProfiles("test") | ||
| @DisplayName("[통합] BookChangeSavedController 테스트") |
|
|
||
| @Transactional | ||
| @Override | ||
| //todo UserRoom 업데이트 로직 추가 필요!! |
seongjunnoh
left a comment
There was a problem hiding this comment.
가독성있게 코드 작성해주셔서 편하게 리뷰할 수 있었습니다!
기록 생성 시 변경되어야 하는 여러 도메인들이 있어서 이 부분은 지금처럼 하나의 트랜잭션 내부에서 동기적으로 처리하는게 맞는건지, 아니면 TODO에 언급해주신 것처럼 스케줄러, 아니면 메시지 큐 등을 활용해서 비동기적으로 처리하는게 좋을지를 의논해보면 좋을 것 같습니다!
관련 내용을 멘토님께 여쭤봐도 좋을 것 같습니다!
| public void validateOverview(int totalPageCount) { | ||
| // 총평 기록 생성 요청인데 page가 책의 전체 페이지 수가 아니라면 에러 | ||
| if (isOverview && page != totalPageCount) { | ||
| String message = String.format( | ||
| "총평(isOverview)은 책의 전체 페이지 수(%d)와 동일한 페이지에서만 작성할 수 있습니다. 현재 페이지 = %d", | ||
| totalPageCount, page | ||
| ); | ||
| throw new InvalidStateException(RECORD_CANNOT_BE_OVERVIEW, new IllegalArgumentException(message)); | ||
| } | ||
| } | ||
|
|
||
| public void validatePage(int totalPageCount) { | ||
| if (page < 1 || page > totalPageCount) { | ||
| String message = String.format( | ||
| "페이지 범위가 잘못되었습니다. 현재 기록할 page = %d, 책 전체 page = %d", | ||
| page, totalPageCount | ||
| ); | ||
| throw new InvalidStateException(INVALID_RECORD_PAGE_RANGE, | ||
| new IllegalArgumentException(message) | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
LGTM 저도 투표 관련 validation 로직 참고해서 수정해보겠습니다!
| @Test | ||
| @DisplayName("[page]가 누락되었을 때 400 Bad Request 반환") | ||
| void record_create_page_null() throws Exception { |
There was a problem hiding this comment.
p3 : '기록 생성 api' 의 통합테스트와 controller 단위 테스트가 하나의 클래스 안에 모인 것 같은데, 이것 또한 분리하시는 건 어떤가요?
'방 생성 api' 에서 api 통합 테스트 코드와, controller 단위 테스트 코드를 분리해놓았긴 합니다!
…into feat/#45-create-record
|



#️⃣ 연관된 이슈
📝 작업 내용
기록 생성 api 흐름
📸 스크린샷
💬 추가사항
📌 PR 진행 시 이러한 점들을 참고해 주세요
Summary by CodeRabbit
Summary by CodeRabbit
신규 기능
버그 수정
테스트
문서화