Conversation
- 추후 Category 에 '대표 이미지 url' 을 추가해야함!!
- 추천하는 다른 모임방 정보 조회시 QueryDSL 활용 - 추후 Room 에 현재 참여중인 사람 수 정보 추가해야 함!!
|
""" Walkthrough모집 중인 방 상세보기 API 및 관련 비즈니스 로직이 새롭게 도입되었습니다. 방 상세 정보, 참가자, 추천 방 목록 조회 기능이 추가되었으며, 이를 위한 도메인, 서비스, 포트, 어댑터, 컨트롤러, DTO, 테스트 코드가 일괄적으로 구현되었습니다. 날짜 유틸리티도 확장되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Controller
participant Service
participant RoomPort
participant BookPort
participant UserRoomPort
participant QueryPort
User->>Controller: GET /rooms/{roomId}/recruiting (userId)
Controller->>Service: getRecruitingRoomDetailView(userId, roomId)
Service->>RoomPort: findById(roomId)
Service->>BookPort: findById(bookId)
Service->>UserRoomPort: findAllByRoomId(roomId)
Service->>QueryPort: findOtherRecruitingRoomsByCategoryOrderByStartDateAsc(room, 5)
Service-->>Controller: RoomRecruitingDetailViewResponse
Controller-->>User: BaseResponse<RoomRecruitingDetailViewResponse>
Assessment against linked issues
Assessment against linked issues: Out-of-scope changesNo out-of-scope changes detected. Possibly related PRs
Suggested labels
Suggested reviewers
Poem
✨ Finishing Touches
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. 🪧 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: 0
🧹 Nitpick comments (5)
src/main/java/konkuk/thip/room/adapter/in/web/response/RoomRecruitingDetailViewResponse.java (1)
5-33: response DTO의 구조 개선을 고려해보세요.현재 구조는 기능적으로 정확하지만, 24개의 필드를 가진 단일 record는 복잡도가 높습니다. 관련 필드들을 그룹화하여 가독성을 개선할 수 있습니다.
예시 구조 개선안:
public record RoomRecruitingDetailViewResponse( boolean isHost, boolean isJoining, - Long roomId, - String roomName, - String roomImageUrl, - boolean isPublic, - String progressStartDate, - String progressEndDate, - String recruitEndDate, - String category, - String roomDescription, - int memberCount, - int recruitCount, - String isbn, - String bookImageUrl, - String bookTitle, - String authorName, - String bookDescription, + RoomInfo roomInfo, + BookInfo bookInfo, List<RecommendRoom> recommendRooms ) { + public record RoomInfo( + Long roomId, + String roomName, + String roomImageUrl, + boolean isPublic, + String progressStartDate, + String progressEndDate, + String recruitEndDate, + String category, + String roomDescription, + int memberCount, + int recruitCount + ) {} + + public record BookInfo( + String isbn, + String bookImageUrl, + String bookTitle, + String authorName, + String bookDescription + ) {}src/main/java/konkuk/thip/room/application/service/RoomShowRecruitingDetailViewService.java (2)
24-24: 상수 정의 위치 개선 제안
RECOMMEND_ROOM_COUNT상수를 클래스 최상단에 정의하는 것보다는 설정 파일이나 별도의 상수 클래스로 분리하는 것을 고려해보세요. 이렇게 하면 비즈니스 요구사항 변경 시 더 유연하게 대응할 수 있습니다.- private final static int RECOMMEND_ROOM_COUNT = 5;application.yml에 추가:
thip: room: recommend-count: 5그리고 @value 어노테이션으로 주입:
@Value("${thip.room.recommend-count:5}") private final int recommendRoomCount;
61-61: 미구현 기능 추적을 위한 TODO 주석 추가 제안현재
null로 하드코딩된 roomImageUrl에 대해 더 명확한 TODO 주석을 추가하는 것을 제안합니다.- null, // roomImageUrl 미구현 + null, // TODO: #이슈번호 - Category 엔티티의 imageUrl 필드 추가 후 구현 예정src/test/java/konkuk/thip/room/adapter/in/web/RoomRecruitingDetailViewApiTest.java (2)
72-99: 테스트 헬퍼 메서드 리팩토링 제안
saveScienceRoom과saveLiteratureRoom메서드 간에 코드 중복이 많습니다. 공통 로직을 추출하여 중복을 제거하는 것을 제안합니다.+ private RoomJpaEntity saveRoom(String bookTitle, String isbn, String roomName, + LocalDate startDate, int recruitCount, boolean isScience) { + AliasJpaEntity alias = aliasJpaRepository.save( + isScience ? TestEntityFactory.createScienceAlias() : TestEntityFactory.createLiteratureAlias()); + + BookJpaEntity book = bookJpaRepository.save(BookJpaEntity.builder() + .title(bookTitle) + .isbn(isbn) + .authorName("한강") + .bestSeller(false) + .publisher("문학동네") + .imageUrl("https://image1.jpg") + .pageCount(300) + .description("한강의 소설") + .build()); + + CategoryJpaEntity category = categoryJpaRepository.save( + isScience ? TestEntityFactory.createScienceCategory(alias) + : TestEntityFactory.createLiteratureCategory(alias)); + + return roomJpaRepository.save(RoomJpaEntity.builder() + .title(roomName) + .description("한강 작품 읽기 모임") + .isPublic(true) + .roomPercentage(0.0) + .startDate(startDate) + .endDate(LocalDate.now().plusDays(30)) + .recruitCount(recruitCount) + .bookJpaEntity(book) + .categoryJpaEntity(category) + .build()); + } - private RoomJpaEntity saveScienceRoom(String bookTitle, String isbn, String roomName, LocalDate startDate, int recruitCount) { + private RoomJpaEntity saveScienceRoom(String bookTitle, String isbn, String roomName, LocalDate startDate, int recruitCount) { + return saveRoom(bookTitle, isbn, roomName, startDate, recruitCount, true); - // existing implementation } - private RoomJpaEntity saveLiteratureRoom(String bookTitle, String isbn, String roomName, LocalDate startDate, int recruitCount) { + private RoomJpaEntity saveLiteratureRoom(String bookTitle, String isbn, String roomName, LocalDate startDate, int recruitCount) { + return saveRoom(bookTitle, isbn, roomName, startDate, recruitCount, false); - // existing implementation }Also applies to: 101-128
211-217: 호스트 역할 변경 로직 개선 제안호스트 테스트를 위한 UserRoomRole 변경 로직이 복잡합니다. 테스트의 가독성을 위해 별도 헬퍼 메서드로 추출하는 것을 제안합니다.
+ private UserJpaEntity changeUserToHost(RoomJpaEntity room, int userIndex) { + UserRoomJpaEntity targetMember = userRoomJpaRepository.findAllByRoomJpaEntity_RoomId(room.getRoomId()).get(userIndex); + userRoomJpaRepository.delete(targetMember); + UserRoomJpaEntity roomCreator = userRoomJpaRepository.save(UserRoomJpaEntity.builder() + .userJpaEntity(targetMember.getUserJpaEntity()) + .roomJpaEntity(targetMember.getRoomJpaEntity()) + .userRoomRole(UserRoomRole.HOST) + .build()); + return roomCreator.getUserJpaEntity(); + } //given RoomJpaEntity targetRoom = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), 10); saveUsersToRoom(targetRoom, 4); - UserRoomJpaEntity firstMember = userRoomJpaRepository.findAllByRoomJpaEntity_RoomId(targetRoom.getRoomId()).get(1); - userRoomJpaRepository.delete(firstMember); - UserRoomJpaEntity roomCreator = userRoomJpaRepository.save(UserRoomJpaEntity.builder() - .userJpaEntity(firstMember.getUserJpaEntity()) - .roomJpaEntity(firstMember.getRoomJpaEntity()) - .userRoomRole(UserRoomRole.HOST) - .build()); // firstMember 을 MEMBER -> HOST 로 수정 + UserJpaEntity hostUser = changeUserToHost(targetRoom, 1);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
src/main/java/konkuk/thip/common/util/DateUtil.java(2 hunks)src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java(3 hunks)src/main/java/konkuk/thip/room/adapter/in/web/response/RoomRecruitingDetailViewResponse.java(1 hunks)src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java(2 hunks)src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryRepository.java(1 hunks)src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryRepositoryImpl.java(2 hunks)src/main/java/konkuk/thip/room/application/port/in/RoomShowRecruitingDetailViewUseCase.java(1 hunks)src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java(1 hunks)src/main/java/konkuk/thip/room/application/service/RoomShowRecruitingDetailViewService.java(1 hunks)src/main/java/konkuk/thip/user/domain/RoomParticipants.java(1 hunks)src/test/java/konkuk/thip/room/adapter/in/web/RoomRecruitingDetailViewApiTest.java(1 hunks)src/test/java/konkuk/thip/user/domain/RoomParticipantsTest.java(1 hunks)
🧰 Additional context used
🧠 Learnings (6)
📓 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/room/application/port/out/RoomQueryPort.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/room/adapter/out/persistence/RoomQueryRepository.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/room/adapter/out/persistence/RoomQueryRepositoryImpl.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/room/adapter/in/web/RoomQueryController.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/room/adapter/in/web/RoomRecruitingDetailViewApiTest.java (1)
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#57
File: src/test/java/konkuk/thip/room/domain/RoomTest.java:0-0
Timestamp: 2025-07-08T16:30:33.771Z
Learning: Room 도메인에서 startDate는 현재 날짜 이후여야 하는 도메인 규칙이 있어서, 테스트에서 만료된 상태를 시뮬레이션하려면 reflection을 사용해야 한다.
🧬 Code Graph Analysis (1)
src/test/java/konkuk/thip/room/adapter/in/web/RoomRecruitingDetailViewApiTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
TestEntityFactory(16-115)
⏰ 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
🔇 Additional comments (20)
src/main/java/konkuk/thip/common/util/DateUtil.java (3)
8-8: 새로운 DateTimeFormatter 추가 승인새로운
DATE_FORMATTER상수 추가가 적절합니다. 일관된 날짜 포맷팅을 위한 좋은 접근입니다.Also applies to: 13-13
36-36: formatAfterTime 메서드 개선 승인trailing space 제거로 더 깔끔한 문자열 반환이 가능해졌습니다.
Also applies to: 41-41, 45-46
48-50: 새로운 formatDate 메서드 승인간단하고 명확한 날짜 포맷팅 유틸리티 메서드입니다. DATE_FORMATTER를 재사용하는 것이 좋습니다.
src/main/java/konkuk/thip/room/application/port/in/RoomShowRecruitingDetailViewUseCase.java (1)
1-8: Use Case 인터페이스 설계 승인헥사고날 아키텍처 패턴을 잘 따르고 있습니다. 메서드명이 명확하고 파라미터와 반환 타입이 적절합니다.
src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java (1)
3-6: CQRS 패턴 준수한 QueryPort 메서드 추가 승인프로젝트의 CQRS 컨벤션을 잘 따르고 있습니다. QueryPort에 response 데이터를 조회하는 메서드가 적절히 추가되었습니다. 메서드명도 의도를 명확히 표현하고 있습니다.
Also applies to: 11-11, 18-18
src/test/java/konkuk/thip/user/domain/RoomParticipantsTest.java (4)
13-22: 테스트 헬퍼 메서드 구현 승인
createUserRoom헬퍼 메서드가 테스트 데이터 생성을 일관되게 처리하고 있어 좋습니다. 코드 중복을 줄이고 가독성을 향상시킵니다.
24-37: 멤버 수 계산 테스트 승인멤버 카운트 계산 로직에 대한 테스트가 적절합니다. Given-When-Then 패턴을 잘 따르고 있습니다.
39-49: 방 참가 여부 확인 테스트 승인참가 중인 유저와 참가하지 않은 유저에 대한 검증을 모두 포함하여 테스트 커버리지가 좋습니다.
51-63: 호스트 권한 확인 테스트 승인HOST와 MEMBER 역할을 구분하는 로직과 존재하지 않는 유저에 대한 검증까지 포함하여 엣지 케이스를 잘 다루고 있습니다.
src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryRepository.java (1)
3-3: Repository 메서드 추가 승인새로운 추천 방 조회 메서드가 적절히 추가되었습니다. 메서드명이 의도를 명확히 표현하고 있으며, 파라미터와 반환 타입이 적절합니다. 기존 코드를 변경하지 않고 순수하게 추가만 된 점도 좋습니다.
Also applies to: 8-8, 14-14
src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java (1)
33-36: LGTM! 깔끔한 어댑터 구현입니다.새로운 추천 방 조회 메서드가 적절하게 구현되었습니다. 단순히 repository로 위임하는 깔끔한 어댑터 패턴을 따르고 있으며, CQRS 컨벤션에도 부합합니다.
src/main/java/konkuk/thip/user/domain/RoomParticipants.java (1)
9-36: LGTM! 훌륭한 일급 컬렉션 구현입니다.
RoomParticipants클래스가 도메인 로직을 잘 캡슐화하고 있습니다:
- 일급 컬렉션 패턴의 올바른 적용
- 명확한 메서드명과 단일 책임 원칙 준수
- Stream API를 활용한 깔끔한 구현
- 적절한 한국어 주석으로 코드 의도 명시
도메인 레이어에서 비즈니스 로직을 관리하는 좋은 예시입니다.
src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryRepositoryImpl.java (1)
143-169: memberCount.intValue() 캐스팅 안전성 확인 완료
RoomCreateRequest에@Max(30)으로 모집 인원을 제한하고 있어, 추천 방의 실제 참여자 수도 최대 30명으로 보장됩니다. 따라서 Long→int 캐스팅에서 overflow 위험이 없습니다.src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java (2)
23-24: 의존성 주입이 적절하게 구성되었습니다.새로운 UseCase들이 올바르게 주입되어 있으며, 필드 순서도 일관성 있게 배치되었습니다.
44-49: LGTM! REST API 설계가 깔끔합니다.새로운 엔드포인트가 잘 구현되었습니다:
- RESTful한 URL 설계 (
/rooms/{roomId}/recruiting)- @userid 어노테이션으로 인증된 사용자 정보 주입
- 일관된 BaseResponse 래핑
- 명확한 메서드명과 파라미터 구조
컨트롤러 레이어의 책임 분리가 적절하게 이루어졌습니다.
src/main/java/konkuk/thip/room/application/service/RoomShowRecruitingDetailViewService.java (2)
33-47: 비즈니스 로직 구현 승인서비스의 핵심 비즈니스 로직이 명확하게 단계별로 구현되어 있습니다. Room과 Book 조회, RoomParticipants 일급 컬렉션 생성, 추천 방 조회의 흐름이 잘 정리되어 있고, 적절한 도메인 객체 위임이 이루어져 있습니다.
35-36: CQRS 포트 사용 컨벤션 일치 확인
RoomCommandPort.findById 및 BookCommandPort.findById 호출은 CommandPort의 findByXXX 규칙에 부합하며, QueryPort에서는 조회 API 전용 메서드(findOtherRecruitingRoomsByCategoryOrderByStartDateAsc 등)를 제공하고 있어 CQRS 컨벤션과 일치합니다. 해당 코드에 수정이 필요 없습니다.src/test/java/konkuk/thip/room/adapter/in/web/RoomRecruitingDetailViewApiTest.java (3)
158-203: 테스트 시나리오 구현 품질 양호첫 번째 테스트 케이스가 API의 핵심 기능을 잘 검증하고 있습니다:
- 사용자 권한 상태 (isHost, isJoining) 확인
- 방 정보와 책 정보 정확성 검증
- 추천 방의 개수와 정렬 순서 확인 (모집 마감 임박 순)
각 assertion이 명확하고 비즈니스 요구사항을 잘 반영하고 있습니다.
258-308: 최대 추천 개수 제한 테스트 검증추천 방이 5개로 제한되는 시나리오를 잘 테스트하고 있습니다. 6개의 추천 가능한 방을 생성하고 실제로 5개만 반환되는지, 그리고 정렬 순서가 올바른지 확인하는 로직이 적절합니다.
178-179: 불필요한 도메인 검증 적용 제외
RoomJpaEntity 빌더를 통해 직접 JPA 엔티티를 저장하기 때문에 도메인 레벨의startDate > now()검증이 적용되지 않습니다. 이 테스트는 만료된 방이 조회되지 않는 쿼리 필터링 로직을 검증하기 위한 것으로, 리플렉션이나 별도 테스트 전용 팩토리 메서드를 도입할 필요가 없습니다.Likely an incorrect or invalid review comment.
|
|
||
| @Getter | ||
| @RequiredArgsConstructor | ||
| public class RoomParticipants { | ||
| /** | ||
| * 특정 Room 과 연관된 UserRoom 들을 모은 일급 컬렉션 | ||
| */ | ||
|
|
||
| private final List<UserRoom> participants; | ||
|
|
||
| public static RoomParticipants from(List<UserRoom> participants) { | ||
| return new RoomParticipants(participants); | ||
| } | ||
|
|
||
| public int calculateMemberCount() { | ||
| return participants.size(); | ||
| } | ||
|
|
||
| public boolean isJoiningToRoom(Long userId) { | ||
| return participants.stream() | ||
| .anyMatch(userRoom -> userRoom.getUserId().equals(userId)); | ||
| } | ||
|
|
||
| public boolean isHostOfRoom(Long userId) { | ||
| return participants.stream() | ||
| .filter(userRoom -> userRoom.getUserId().equals(userId)) | ||
| .anyMatch(userRoom -> userRoom.getUserRoomRole().equals(UserRoomRole.HOST.getType())); | ||
| } | ||
| } |
There was a problem hiding this comment.
p3: 특정 Room과 연관된 UserRoom들이니까 Room 도메인에 속하지 않을까 싶은데 User 패키지 아래 두신 이유가 따로있나욥
There was a problem hiding this comment.
딱히 이유 없습니다! 그냥 UserRoom 의 List를 포함하는 일급컬렉션이어서 UserRoom 과 같은 패키지에 위치시켰습니다!
| return tuples.stream() | ||
| .map(t -> new RoomRecruitingDetailViewResponse.RecommendRoom( | ||
| null, // roomImageUrl은 추후 구현 | ||
| t.get(room.title), | ||
| t.get(memberCountExpr).intValue(), | ||
| t.get(room.recruitCount), | ||
| DateUtil.formatAfterTime(t.get(room.startDate)) | ||
| )) | ||
| .toList(); |
There was a problem hiding this comment.
p3: 각 필드를 명확히 알 수 있게 빌더 패턴 적용하면 좋을 것 같습니다!!
| private RoomRecruitingDetailViewResponse buildResponse( | ||
| Long userId, | ||
| Room room, | ||
| Book book, | ||
| RoomParticipants participants, | ||
| List<RoomRecruitingDetailViewResponse.RecommendRoom> recommendRooms | ||
| ) { | ||
| return new RoomRecruitingDetailViewResponse( | ||
| participants.isHostOfRoom(userId), | ||
| participants.isJoiningToRoom(userId), | ||
| room.getId(), | ||
| room.getTitle(), | ||
| null, // roomImageUrl 미구현 | ||
| room.isPublic(), | ||
| DateUtil.formatDate(room.getStartDate()), | ||
| DateUtil.formatDate(room.getEndDate()), | ||
| DateUtil.formatAfterTime(room.getStartDate()), | ||
| room.getCategory().getValue(), | ||
| room.getDescription(), | ||
| participants.calculateMemberCount(), | ||
| room.getRecruitCount(), | ||
| book.getIsbn(), | ||
| book.getImageUrl(), | ||
| book.getTitle(), | ||
| book.getAuthorName(), | ||
| book.getDescription(), | ||
| recommendRooms | ||
| ); | ||
| } |
There was a problem hiding this comment.
p3: 이 메서드는 RoomRecruitingDetailViewResponse의 정적 메서드로 만들어서 생성 책임을 dto가 갖도록 하면 코드가 깔끔해질 것 같아요!
엇 혹시 도메인 규칙을 서비스 레벨에서 사용하고 싶으셔서 이렇게 작성하신 거라면 그대로 놔둬도 될 것 같습니다!
There was a problem hiding this comment.
response dto 에 정적 팩토리 메서드 구현할 경우, 서비스 레벨에서 메서드 호출할 때 넘겨줘야하는 파라미터가 너무 많아지기도 하고, response dto 내부에서 또 builder를 정의해주어야 해서 service의 private 메서드로 분리하긴 했습니다
| participants.isHostOfRoom(userId), | ||
| participants.isJoiningToRoom(userId), |
There was a problem hiding this comment.
p3: 두 메서드의 조건이 중복되는 것 같은데 isJoiningToRoom(user)를 먼저 호출하여 true일 경우에만 isHostOfRoom(userId)을 호출하는거 어떨까요?
단순 의견이라 꼭 적용할 필요는 없습니다!
There was a problem hiding this comment.
response를 구성할때 if 분기처리를 통해서 isJoining 이 true인 경우에만 isHost 메서드를 호출하는 흐름을 말하시는거 같은데, response를 구성할때 분기처리 로직을 추가하는거 보다, 단순히 메서드를 호출하는게 더 가독성이 있지않나 싶어서 이렇게 구현했긴합니다!
| @RequiredArgsConstructor | ||
| public class RoomParticipants { | ||
| /** | ||
| * 특정 Room 과 연관된 UserRoom 들을 모은 일급 컬렉션 |
There was a problem hiding this comment.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
src/main/java/konkuk/thip/common/util/DateUtil.java(2 hunks)src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java(3 hunks)src/main/java/konkuk/thip/room/adapter/in/web/response/RoomRecruitingDetailViewResponse.java(1 hunks)src/main/java/konkuk/thip/room/adapter/out/jpa/CategoryJpaEntity.java(1 hunks)src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java(2 hunks)src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryRepository.java(1 hunks)src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryRepositoryImpl.java(2 hunks)src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java(1 hunks)src/main/java/konkuk/thip/room/application/service/RoomShowRecruitingDetailViewService.java(1 hunks)src/main/java/konkuk/thip/room/domain/Category.java(1 hunks)src/test/java/konkuk/thip/common/util/TestEntityFactory.java(2 hunks)src/test/java/konkuk/thip/vote/adapter/in/web/VoteCreateControllerTest.java(1 hunks)
✅ Files skipped from review due to trivial changes (2)
- src/test/java/konkuk/thip/vote/adapter/in/web/VoteCreateControllerTest.java
- src/test/java/konkuk/thip/common/util/TestEntityFactory.java
🚧 Files skipped from review as they are similar to previous changes (8)
- src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java
- src/main/java/konkuk/thip/common/util/DateUtil.java
- src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java
- src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryRepository.java
- src/main/java/konkuk/thip/room/adapter/in/web/response/RoomRecruitingDetailViewResponse.java
- src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryRepositoryImpl.java
- src/main/java/konkuk/thip/room/application/service/RoomShowRecruitingDetailViewService.java
- src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java
🧰 Additional context used
🧠 Learnings (1)
📓 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로부터 조회하는 메서드를 추가함.
⏰ 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
🔇 Additional comments (2)
src/main/java/konkuk/thip/room/adapter/out/jpa/CategoryJpaEntity.java (1)
25-26: 마이그레이션 파일 누락 여부 직접 확인 필요
위 스크립트로 SQL/YAML/XML 형식의 마이그레이션 파일에서category테이블 정의를 찾을 수 없었습니다. 신규 non-nullable 필드 추가 시 기존 데이터에 영향을 줄 수 있으므로,
- 프로젝트에 Flyway/Liquibase 등 마이그레이션 스크립트가 실제 존재하는지
- 해당 스크립트에
category테이블 변경 내역(신규image_url컬럼 추가)이 포함되어 있는지를 수동으로 검토해 주세요.
필요하다면 기본값 지정, 데이터 백필(backfill), 단계적 null 허용 등 적절한 마이그레이션 전략을 구현해 주시기 바랍니다.src/main/java/konkuk/thip/room/domain/Category.java (1)
26-26: LGTM!새로운
imageUrl필드가 올바르게 선언되었습니다.
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/test/java/konkuk/thip/room/adapter/in/web/RoomRecruitingDetailViewApiTest.java (1)
212-218: UserRoom 역할 변경 로직 개선 가능UserRoom을 삭제하고 재생성하는 대신, 기존 엔티티의 역할을 직접 수정하는 것이 더 간단합니다.
-UserRoomJpaEntity firstMember = userRoomJpaRepository.findAllByRoomJpaEntity_RoomId(targetRoom.getRoomId()).get(1); -userRoomJpaRepository.delete(firstMember); -UserRoomJpaEntity roomCreator = userRoomJpaRepository.save(UserRoomJpaEntity.builder() - .userJpaEntity(firstMember.getUserJpaEntity()) - .roomJpaEntity(firstMember.getRoomJpaEntity()) - .userRoomRole(UserRoomRole.HOST) - .build()); +UserRoomJpaEntity firstMember = userRoomJpaRepository.findAllByRoomJpaEntity_RoomId(targetRoom.getRoomId()).get(1); +firstMember.changeRole(UserRoomRole.HOST); // 또는 setter 사용 +UserRoomJpaEntity roomCreator = userRoomJpaRepository.save(firstMember);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/main/java/konkuk/thip/room/domain/Category.java(1 hunks)src/test/java/konkuk/thip/room/adapter/in/web/RoomRecruitingDetailViewApiTest.java(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/java/konkuk/thip/room/domain/Category.java
🧰 Additional context used
🧠 Learnings (2)
📓 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/in/web/RoomRecruitingDetailViewApiTest.java (1)
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#57
File: src/test/java/konkuk/thip/room/domain/RoomTest.java:0-0
Timestamp: 2025-07-08T16:30:33.771Z
Learning: Room 도메인에서 startDate는 현재 날짜 이후여야 하는 도메인 규칙이 있어서, 테스트에서 만료된 상태를 시뮬레이션하려면 reflection을 사용해야 한다.
⏰ 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
🔇 Additional comments (3)
src/test/java/konkuk/thip/room/adapter/in/web/RoomRecruitingDetailViewApiTest.java (3)
37-37: 보안 필터 비활성화 확인 필요
@AutoConfigureMockMvc(addFilters = false)로 모든 필터를 비활성화하고 있습니다. 이는 실제 환경의 보안 검증을 우회할 수 있습니다. 테스트 목적상 필요한 경우라면 주석으로 이유를 명시하는 것이 좋겠습니다.
131-131: 모든 사용자에게 동일한 Science alias 할당 확인모든 사용자에게 Science alias를 할당하고 있습니다. 실제 비즈니스 로직에서도 방 참가자들이 모두 동일한 관심사 alias를 가져야 하는지 확인이 필요합니다.
190-190: roomImageUrl 검증 값 일치 확인테스트에서 검증한
"과학/IT_image"값이
TestEntityFactory.createScienceCategory()에서 설정된 imageUrl"과학/IT_image"과 일치함을 확인했습니다.
해당 부분은 그대로 두셔도 됩니다.
| RoomJpaEntity recruit_expired_room_4 = saveScienceRoom("과학-책", "isbn6", "모집기한-지난-과학방", LocalDate.now().minusDays(1), 8); | ||
| saveUsersToRoom(recruit_expired_room_4, 6); |
There was a problem hiding this comment.
도메인 규칙 위반: 과거 startDate 설정
startDate를 과거(LocalDate.now().minusDays(1))로 설정하고 있는데, Room 도메인 규칙상 startDate는 현재 날짜 이후여야 합니다. 모집 기한이 지난 방을 테스트하려면 reflection을 사용해야 합니다.
이전 학습 내용을 참고하여 reflection을 사용한 방법을 적용하거나, 다른 방법으로 모집 기한 만료 상태를 시뮬레이션해야 합니다.
🤖 Prompt for AI Agents
In
src/test/java/konkuk/thip/room/adapter/in/web/RoomRecruitingDetailViewApiTest.java
around lines 178 to 179, the test sets startDate to a past date violating the
Room domain rule that startDate must be today or later. To fix this, create the
Room entity with a valid startDate (today or future), then use reflection to
modify the startDate field to a past date to simulate an expired recruiting
period without breaking domain constraints.
| private RoomJpaEntity saveScienceRoom(String bookTitle, String isbn, String roomName, LocalDate startDate, int recruitCount) { | ||
| AliasJpaEntity alias = aliasJpaRepository.save(TestEntityFactory.createScienceAlias()); | ||
|
|
||
| BookJpaEntity book = bookJpaRepository.save(BookJpaEntity.builder() | ||
| .title(bookTitle) | ||
| .isbn(isbn) | ||
| .authorName("한강") | ||
| .bestSeller(false) | ||
| .publisher("문학동네") | ||
| .imageUrl("https://image1.jpg") | ||
| .pageCount(300) | ||
| .description("한강의 소설") | ||
| .build()); | ||
|
|
||
| CategoryJpaEntity category = categoryJpaRepository.save(TestEntityFactory.createScienceCategory(alias)); | ||
|
|
||
| return roomJpaRepository.save(RoomJpaEntity.builder() | ||
| .title(roomName) | ||
| .description("한강 작품 읽기 모임") | ||
| .isPublic(true) | ||
| .roomPercentage(0.0) | ||
| .startDate(startDate) | ||
| .endDate(LocalDate.now().plusDays(30)) | ||
| .recruitCount(recruitCount) | ||
| .bookJpaEntity(book) | ||
| .categoryJpaEntity(category) | ||
| .build()); | ||
| } | ||
|
|
||
| private RoomJpaEntity saveLiteratureRoom(String bookTitle, String isbn, String roomName, LocalDate startDate, int recruitCount) { | ||
| AliasJpaEntity alias = aliasJpaRepository.save(TestEntityFactory.createLiteratureAlias()); | ||
|
|
||
| BookJpaEntity book = bookJpaRepository.save(BookJpaEntity.builder() | ||
| .title(bookTitle) | ||
| .isbn(isbn) | ||
| .authorName("한강") | ||
| .bestSeller(false) | ||
| .publisher("문학동네") | ||
| .imageUrl("https://image1.jpg") | ||
| .pageCount(300) | ||
| .description("한강의 소설") | ||
| .build()); | ||
|
|
||
| CategoryJpaEntity category = categoryJpaRepository.save(TestEntityFactory.createLiteratureCategory(alias)); | ||
|
|
||
| return roomJpaRepository.save(RoomJpaEntity.builder() | ||
| .title(roomName) | ||
| .description("한강 작품 읽기 모임") | ||
| .isPublic(true) | ||
| .roomPercentage(0.0) | ||
| .startDate(startDate) | ||
| .endDate(LocalDate.now().plusDays(30)) | ||
| .recruitCount(recruitCount) | ||
| .bookJpaEntity(book) | ||
| .categoryJpaEntity(category) | ||
| .build()); | ||
| } | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
헬퍼 메서드 중복 제거 필요
saveScienceRoom과 saveLiteratureRoom 메서드가 거의 동일한 구조를 가지고 있습니다. 카테고리 타입을 파라미터로 받는 하나의 메서드로 통합하면 코드 중복을 제거할 수 있습니다.
-private RoomJpaEntity saveScienceRoom(String bookTitle, String isbn, String roomName, LocalDate startDate, int recruitCount) {
- AliasJpaEntity alias = aliasJpaRepository.save(TestEntityFactory.createScienceAlias());
- // ... 중복 코드 ...
-}
-
-private RoomJpaEntity saveLiteratureRoom(String bookTitle, String isbn, String roomName, LocalDate startDate, int recruitCount) {
- AliasJpaEntity alias = aliasJpaRepository.save(TestEntityFactory.createLiteratureAlias());
- // ... 중복 코드 ...
-}
+private RoomJpaEntity saveRoom(CategoryType categoryType, String bookTitle, String isbn, String roomName, LocalDate startDate, int recruitCount) {
+ AliasJpaEntity alias = aliasJpaRepository.save(
+ categoryType == CategoryType.SCIENCE ? TestEntityFactory.createScienceAlias() : TestEntityFactory.createLiteratureAlias()
+ );
+
+ BookJpaEntity book = bookJpaRepository.save(BookJpaEntity.builder()
+ .title(bookTitle)
+ .isbn(isbn)
+ .authorName("한강")
+ .bestSeller(false)
+ .publisher("문학동네")
+ .imageUrl("https://image1.jpg")
+ .pageCount(300)
+ .description("한강의 소설")
+ .build());
+
+ CategoryJpaEntity category = categoryJpaRepository.save(
+ categoryType == CategoryType.SCIENCE ? TestEntityFactory.createScienceCategory(alias) : TestEntityFactory.createLiteratureCategory(alias)
+ );
+
+ return roomJpaRepository.save(RoomJpaEntity.builder()
+ .title(roomName)
+ .description("한강 작품 읽기 모임")
+ .isPublic(true)
+ .roomPercentage(0.0)
+ .startDate(startDate)
+ .endDate(LocalDate.now().plusDays(30))
+ .recruitCount(recruitCount)
+ .bookJpaEntity(book)
+ .categoryJpaEntity(category)
+ .build());
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private RoomJpaEntity saveScienceRoom(String bookTitle, String isbn, String roomName, LocalDate startDate, int recruitCount) { | |
| AliasJpaEntity alias = aliasJpaRepository.save(TestEntityFactory.createScienceAlias()); | |
| BookJpaEntity book = bookJpaRepository.save(BookJpaEntity.builder() | |
| .title(bookTitle) | |
| .isbn(isbn) | |
| .authorName("한강") | |
| .bestSeller(false) | |
| .publisher("문학동네") | |
| .imageUrl("https://image1.jpg") | |
| .pageCount(300) | |
| .description("한강의 소설") | |
| .build()); | |
| CategoryJpaEntity category = categoryJpaRepository.save(TestEntityFactory.createScienceCategory(alias)); | |
| return roomJpaRepository.save(RoomJpaEntity.builder() | |
| .title(roomName) | |
| .description("한강 작품 읽기 모임") | |
| .isPublic(true) | |
| .roomPercentage(0.0) | |
| .startDate(startDate) | |
| .endDate(LocalDate.now().plusDays(30)) | |
| .recruitCount(recruitCount) | |
| .bookJpaEntity(book) | |
| .categoryJpaEntity(category) | |
| .build()); | |
| } | |
| private RoomJpaEntity saveLiteratureRoom(String bookTitle, String isbn, String roomName, LocalDate startDate, int recruitCount) { | |
| AliasJpaEntity alias = aliasJpaRepository.save(TestEntityFactory.createLiteratureAlias()); | |
| BookJpaEntity book = bookJpaRepository.save(BookJpaEntity.builder() | |
| .title(bookTitle) | |
| .isbn(isbn) | |
| .authorName("한강") | |
| .bestSeller(false) | |
| .publisher("문학동네") | |
| .imageUrl("https://image1.jpg") | |
| .pageCount(300) | |
| .description("한강의 소설") | |
| .build()); | |
| CategoryJpaEntity category = categoryJpaRepository.save(TestEntityFactory.createLiteratureCategory(alias)); | |
| return roomJpaRepository.save(RoomJpaEntity.builder() | |
| .title(roomName) | |
| .description("한강 작품 읽기 모임") | |
| .isPublic(true) | |
| .roomPercentage(0.0) | |
| .startDate(startDate) | |
| .endDate(LocalDate.now().plusDays(30)) | |
| .recruitCount(recruitCount) | |
| .bookJpaEntity(book) | |
| .categoryJpaEntity(category) | |
| .build()); | |
| } | |
| private RoomJpaEntity saveRoom(CategoryType categoryType, | |
| String bookTitle, | |
| String isbn, | |
| String roomName, | |
| LocalDate startDate, | |
| int recruitCount) { | |
| AliasJpaEntity alias = aliasJpaRepository.save( | |
| categoryType == CategoryType.SCIENCE | |
| ? TestEntityFactory.createScienceAlias() | |
| : TestEntityFactory.createLiteratureAlias() | |
| ); | |
| BookJpaEntity book = bookJpaRepository.save(BookJpaEntity.builder() | |
| .title(bookTitle) | |
| .isbn(isbn) | |
| .authorName("한강") | |
| .bestSeller(false) | |
| .publisher("문학동네") | |
| .imageUrl("https://image1.jpg") | |
| .pageCount(300) | |
| .description("한강의 소설") | |
| .build()); | |
| CategoryJpaEntity category = categoryJpaRepository.save( | |
| categoryType == CategoryType.SCIENCE | |
| ? TestEntityFactory.createScienceCategory(alias) | |
| : TestEntityFactory.createLiteratureCategory(alias) | |
| ); | |
| return roomJpaRepository.save(RoomJpaEntity.builder() | |
| .title(roomName) | |
| .description("한강 작품 읽기 모임") | |
| .isPublic(true) | |
| .roomPercentage(0.0) | |
| .startDate(startDate) | |
| .endDate(LocalDate.now().plusDays(30)) | |
| .recruitCount(recruitCount) | |
| .bookJpaEntity(book) | |
| .categoryJpaEntity(category) | |
| .build()); | |
| } |
🤖 Prompt for AI Agents
In
src/test/java/konkuk/thip/room/adapter/in/web/RoomRecruitingDetailViewApiTest.java
between lines 72 and 129, the methods saveScienceRoom and saveLiteratureRoom
have nearly identical code except for the category type and alias creation.
Refactor by creating a single helper method that accepts parameters for the
alias and category creation functions or types, then use this method to save
rooms for different categories. This will remove duplication and centralize the
room-saving logic.



#️⃣ 연관된 이슈
📝 작업 내용
모집중인 방 상세보기 api를 개발하였습니다
api 주요 로직
수정된 기획(= 방 대표 이미지 도입 = 카테고리의 대표 이미지) 으로 인해 Category 에 imageUrl 필드가 추가되어야 합니다.
다른 분들과의 충돌을 우려해 아직 수정하지는 않았습니다. 개발중인 브랜치들 모두 머지되면 제가 수정하겠습니다
📸 스크린샷
💬 리뷰 요구사항
📌 PR 진행 시 이러한 점들을 참고해 주세요
Summary by CodeRabbit
신규 기능
버그 수정
테스트