Skip to content

[feat] 기록 생성 api 개발#50

Merged
seongjunnoh merged 30 commits intodevelopfrom
feat/#45-create-record
Jul 8, 2025
Merged

[feat] 기록 생성 api 개발#50
seongjunnoh merged 30 commits intodevelopfrom
feat/#45-create-record

Conversation

@buzz0331
Copy link
Contributor

@buzz0331 buzz0331 commented Jul 5, 2025

#️⃣ 연관된 이슈

closes #45

📝 작업 내용

기록 생성 api 흐름

  1. controller : 유효성 검증 & request dto -> command로 매핑 & Usecase로 command 전달
  2. application : Record 도메인 생성 및 유효성 검증 & commandport로 영속화 요청
  3. persistence adapter : RecordJpaEntity 저장

📸 스크린샷

스크린샷 2025-07-06 오전 1 03 55

💬 추가사항

  • 성준님이 Vote 생성 api 너무 깔끔하게 작성해주셔서 많이 참고했습니다 ㅎ
  • 추가적으로, 아까 회의에서 말씀드렸던 것처럼 테스트 코드를 작성할 때 클래스 단위로 통합테스트와 단위 테스트를 구분하는 것에 대해서 한번 적용시켜보았는데 이 부분 확인 부탁드릴게요! (별로면 냉정하게 말씀해주셔도 됩니다!)
스크린샷 2025-07-06 오전 1 52 03

📌 PR 진행 시 이러한 점들을 참고해 주세요

* P1 : 꼭 반영해 주세요 (Request Changes) - 이슈가 발생하거나 취약점이 발견되는 케이스 등
* P2 : 반영을 적극적으로 고려해 주시면 좋을 것 같아요 (Comment)
* P3 : 이런 방법도 있을 것 같아요~ 등의 사소한 의견입니다 (Chore)

Summary by CodeRabbit

Summary by CodeRabbit

  • 신규 기능

    • 방에 기록(Record)을 생성하는 API가 추가되었습니다.
    • 기록 생성 요청 및 응답 형식이 명확히 정의되었습니다.
    • 기록 생성 시 페이지 범위 및 총평 여부에 대한 유효성 검사가 도입되었습니다.
    • 사용자-방 관계 조회 및 진행도 관리 기능이 추가되었습니다.
    • 기록 저장 및 사용자 진행도 업데이트 로직이 구현되었습니다.
  • 버그 수정

    • 기록 및 사용자-방 관련 오류 코드가 추가되어, 잘못된 요청에 대해 더 명확한 에러 메시지를 제공합니다.
  • 테스트

    • 기록 생성 API와 도메인 유효성 검증에 대한 통합 및 단위 테스트가 추가되었습니다.
    • 여러 테스트 클래스에 명확한 DisplayName 주석이 추가되어 테스트 결과 가독성이 향상되었습니다.
  • 문서화

    • 불필요한 더미 클래스 및 인터페이스가 제거되어 코드가 정리되었습니다.

@coderabbitai
Copy link

coderabbitai bot commented Jul 5, 2025

"""

Walkthrough

기록(Record) 생성 API를 도입하는 대규모 변경입니다. 컨트롤러, 서비스, 포트, 어댑터, 도메인, DTO 및 검증 로직이 추가되었으며, 불필요한 더미 파일들은 삭제되었습니다. 도메인 레벨의 유효성 검사와 예외 코드가 신설되었고, 통합 및 단위 테스트도 함께 추가되었습니다.

Changes

파일/경로 요약 변경 요약
.../common/exception/code/ErrorCode.java RECORD 및 USER_ROOM 관련 에러 코드 5종 추가
.../record/adapter/in/web/RecordCommandController.java 기록 생성 API 엔드포인트 및 의존성 추가
.../record/adapter/in/web/request/RecordCreateRequest.java 기록 생성 요청 DTO 및 도메인 커맨드 변환 메서드 추가
.../record/adapter/in/web/response/RecordCreateResponse.java 기록 생성 응답 DTO 및 정적 팩토리 메서드 추가
.../record/adapter/out/persistence/RecordCommandPersistenceAdapter.java 기록 저장 메서드 구현, User/Room 조회 및 예외 처리 추가
.../record/application/port/in/RecordCreateUseCase.java 기록 생성 유스케이스 인터페이스 추가
.../record/application/port/in/dto/RecordCreateCommand.java 기록 생성 커맨드 DTO 추가
.../record/application/port/out/RecordCommandPort.java 기록 저장 메서드 시그니처 추가
.../record/application/service/RecordCreateService.java 기록 생성 서비스 구현, 도메인 검증 및 저장 로직 추가
.../record/domain/Record.java withoutId 팩토리, validateOverview, validatePage 도메인 검증 메서드 추가
.../record/adapter/in/web/request/DummyRequest.java
.../response/DummyResponse.java
.../application/port/in/DummyUseCase.java
.../application/port/in/dto/DummyCommand.java
.../application/service/RecordService.java
불필요한 더미 클래스/인터페이스 5종 삭제
.../test/java/konkuk/thip/record/adapter/in/web/RecordCreateControllerTest.java 기록 생성 API 통합 테스트 추가, 성공/실패(검증) 케이스 포함
.../test/java/konkuk/thip/record/domain/RecordTest.java 도메인 검증 메서드 단위 테스트 추가
.../test/java/konkuk/thip/user/domain/UserRoomTest.java UserRoom 도메인 메서드 단위 테스트 추가
.../main/java/konkuk/thip/user/domain/UserRoom.java canWriteOverview, updateUserProgress 메서드 추가
.../main/java/konkuk/thip/user/application/port/out/UserRoomCommandPort.java UserRoom 관련 조회 메서드 선언
.../main/java/konkuk/thip/user/adapter/out/persistence/UserRoomCommandPersistenceAdapter.java UserRoom 조회 구현체 추가
.../main/java/konkuk/thip/user/adapter/out/persistence/UserRoomJpaRepository.java UserRoom 조회 쿼리 메서드 추가
.../main/java/konkuk/thip/room/domain/Room.java isExpired, updateRoomPercentage 메서드 추가
.../test/java/konkuk/thip/book/adapter/in/web/BookChangeSavedControllerTest.java
.../BookDetailSearchControllerTest.java
.../BookQueryControllerTest.java
.../book/adapter/out/api/NaverApiUtilTest.java
.../feed/adapter/out/jpa/FeedJpaEntityTest.java
.../room/adapter/out/jpa/RecordJpaEntityTest.java
.../RoomJpaEntityTest.java
.../VoteJpaEntityTest.java
.../user/adapter/in/web/UserSignupControllerTest.java
.../UserVerifyNicknameControllerTest.java
.../UserViewAliasChoiceControllerTest.java
.../user/adapter/out/jpa/UserJpaEntityTest.java
.../vote/adapter/in/web/VoteCreateControllerTest.java
.../vote/domain/VoteTest.java
각종 테스트 클래스에 @DisplayName 어노테이션 추가(테스트 명시성 향상)

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>
Loading

Assessment against linked issues

Objective Addressed Explanation
기록 생성 API를 개발한다 (#45)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
@DisplayName 어노테이션 추가 (src/test/java/…/test 클래스들, 여러 파일) 테스트 명시성 향상 목적이나, 기록 생성 API 개발 요구사항과 직접적 관련 없음.
불필요한 더미 클래스/인터페이스 삭제 (record/adapter/in/web/request/DummyRequest.java 등) 기록 생성 API 개발과 직접적 관련은 없으나, 코드 정리에 해당.

Suggested labels

🐶 희진

Poem

🐇
기록이 탄생한 오늘,
새 페이지 위에 꿈이 춤을 추네.
검증도 꼼꼼, 예외도 촘촘,
API 문을 열고,
토끼는 즐겁게 깡총!
"기록을 남겨라!"
코드와 테스트, 모두 완벽하게!

"""


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dc9212e and a407341.

📒 Files selected for processing (2)
  • 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)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/konkuk/thip/record/adapter/in/web/RecordCommandController.java
  • src/main/java/konkuk/thip/common/exception/code/ErrorCode.java
✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need 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)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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: 도메인 검증 메서드의 호출 시점을 명확히 해주세요.

검증 메서드들이 잘 구현되어 있지만, 언제 호출되는지 명확하지 않습니다. 일반적으로 도메인 객체 생성 시점이나 상태 변경 시점에 자동으로 호출되도록 하는 것이 좋습니다.

다음과 같은 방법을 고려해보세요:

  1. 생성자나 빌더에서 자동 호출
  2. 별도의 validate() 메서드에서 모든 검증 로직을 한 번에 수행
  3. 도메인 서비스에서 명시적 호출 (현재 구현 방식으로 보임)
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

📥 Commits

Reviewing files that changed from the base of the PR and between a6d9ea5 and b16f767.

📒 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: 테스트 정리 로직이 적절하게 구현되었습니다.

의존성 순서에 맞게 엔티티들을 정리하고 있어 테스트 격리가 잘 되어 있습니다.

Comment on lines 91 to 211
@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자 입니다.")));
}
Copy link

Choose a reason for hiding this comment

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

🛠️ 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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 55a7afa and 63e1675.

📒 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 메서드를 통해 도메인 엔티티를 찾아오는 것이 컨벤션입니다. 이 어댑터가 해당 컨벤션을 정확히 따르고 있습니다.

Copy link
Member

@hd0rable hd0rable left a comment

Choose a reason for hiding this comment

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

구현하느라 수고하셨습니다~~ 전체적으로 코드 너무 깔끔해서 보기 좋은것같아요!! 진짜 사소한 리뷰하나 남겼습니다 ㅎ

@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
@DisplayName("[통합] BookChangeSavedController 테스트")
Copy link
Member

Choose a reason for hiding this comment

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

👍🏻👍🏻👍🏻


@Transactional
@Override
//todo UserRoom 업데이트 로직 추가 필요!!
Copy link
Member

Choose a reason for hiding this comment

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

LGTM

seongjunnoh
seongjunnoh previously approved these changes Jul 7, 2025
Copy link
Collaborator

@seongjunnoh seongjunnoh left a comment

Choose a reason for hiding this comment

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

가독성있게 코드 작성해주셔서 편하게 리뷰할 수 있었습니다!

기록 생성 시 변경되어야 하는 여러 도메인들이 있어서 이 부분은 지금처럼 하나의 트랜잭션 내부에서 동기적으로 처리하는게 맞는건지, 아니면 TODO에 언급해주신 것처럼 스케줄러, 아니면 메시지 큐 등을 활용해서 비동기적으로 처리하는게 좋을지를 의논해보면 좋을 것 같습니다!

관련 내용을 멘토님께 여쭤봐도 좋을 것 같습니다!

Comment on lines +43 to +64
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)
);
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

LGTM 저도 투표 관련 validation 로직 참고해서 수정해보겠습니다!

Comment on lines +149 to +151
@Test
@DisplayName("[page]가 누락되었을 때 400 Bad Request 반환")
void record_create_page_null() throws Exception {
Copy link
Collaborator

Choose a reason for hiding this comment

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

p3 : '기록 생성 api' 의 통합테스트와 controller 단위 테스트가 하나의 클래스 안에 모인 것 같은데, 이것 또한 분리하시는 건 어떤가요?

'방 생성 api' 에서 api 통합 테스트 코드와, controller 단위 테스트 코드를 분리해놓았긴 합니다!

@sonarqubecloud
Copy link

sonarqubecloud bot commented Jul 8, 2025

@seongjunnoh seongjunnoh merged commit 2d86b7f into develop Jul 8, 2025
3 checks passed
@seongjunnoh seongjunnoh deleted the feat/#45-create-record branch July 8, 2025 03:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[THIP2025-87] [feat] 기록 생성 api

3 participants