Skip to content

[feat] 내가 참여한 모임방 목록 조회#93

Merged
seongjunnoh merged 26 commits intodevelopfrom
feat/#89-get-room-mine
Jul 28, 2025
Merged

[feat] 내가 참여한 모임방 목록 조회#93
seongjunnoh merged 26 commits intodevelopfrom
feat/#89-get-room-mine

Conversation

@seongjunnoh
Copy link
Collaborator

@seongjunnoh seongjunnoh commented Jul 21, 2025

#️⃣ 연관된 이슈

closes #89

📝 작업 내용

내가 참여한 목록 조회 api 를 개발하였습니다.

<api 플로우>

  • controller
    • type, cursorDate, cursorId 값을 request param 으로 전달받음
    • 위 3개 param은 모두 required=false 로 설정, type은 개발 편의성을 위해 defaultValue 속성 추가
    • response 에 해당 페이지가 첫 페이지인지를 나타내는 boolean first 는 제거하였습니다
      • 이유는 first 값은 클라이언트 측에서 사용하지 않을 것으로 예상 + 커서 기반 페이징 처리 시 first 값을 단순히 cursor 의 null 유무가 아니라 진짜 이전 데이터가 있는지를 확인하려면 추가적인 리소스가 드는데, 불필요하다는 생각이 들었습니다.
      • 관련해서 api 명세서 최신화했습니다
  • service
    • cursor 2개의 null 여부를 xor 연산을 통해 검증 (cursor 값 2개는 모두 있거나, 둘다 없거나 만 유효한 상태로 정의)
    • type request param 의 유효성 검증 (enum 을 통해)
    • 영속성 adapter 로부터 받은 데이터를 통해 resposne 구성 (pageSize 는 10으로 설정)
  • 영속성 코드
    • type 별로 데이터 조회 메서드를 분리 + 커서 기반 페이징 메서드를 분리하여 최대한 QueryDSL 코드를 간결하게 작성하려고 하였음
    • 각 메서드 별로 요구사항에 정의된 필터링 조건, 정렬 조건 을 만족하도록 구현하였음
  • 테스트 코드
    • api 통합 테스트 코드로 테스트 코드를 작성
    • 모든 가능한 type 별로 올바른 응답이 나오는지 테스트 하였음
    • wrong request param 에 대해서 예외 처리가 적절하게 이루어지는지 테스트 하였음
    • 커서 기반 페이징 처리가 잘 이루어지는지 테스트 하였음

📸 스크린샷

💬 리뷰 요구사항

QueryDSL 관련 로직을 담당하는 RoomQueryRepositoryImpl 의 코드가 많이 복잡해지는 것 같아, 커서 기반 페이징 처리를 담당하는 내부 private 메서드를 정의하고, 이를 공유하여 반환값을 구성하도록 하였습니다.

하지만 이렇게 하더라도 여전히 RoomQueryRepositoryImpl 의 코드가 무겁고, 추후 조회 api 개발하면서 코드가 더 추가될텐데, 더 나은 방식은 없을까 고민해보면 좋을 것 같습니다!

api 통합 테스트 코드를 상세하게 작성하였습니다! 테스트 코드의 흐름을 먼저 보시는 것이 리뷰하기에 더 편할 것 같습니다!

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

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

Summary by CodeRabbit

Summary by CodeRabbit

  • 신규 기능

    • 사용자가 참여한 방 목록을 상태별(모집중, 진행중, 모집 및 진행중, 만료됨)로 조회할 수 있는 API가 추가되었습니다.
    • 커서 기반 페이지네이션을 지원하여 대용량 데이터도 효율적으로 탐색할 수 있습니다.
  • 버그 수정

    • 방 목록 조회 시 유효하지 않은 MY ROOM 타입 또는 커서 값에 대해 명확한 오류 메시지와 코드가 반환됩니다.
  • 테스트

    • 방 목록 조회 API에 대한 다양한 시나리오(필터링, 페이징, 입력값 검증 등)를 검증하는 통합 테스트가 포함되었습니다.

@seongjunnoh seongjunnoh linked an issue Jul 21, 2025 that may be closed by this pull request
2 tasks
@coderabbitai
Copy link

coderabbitai bot commented Jul 21, 2025

"""

Walkthrough

유저가 참여한 모임방을 상태별(진행중, 모집중, 완료됨)로 조회하는 새로운 API가 추가되었습니다. 이를 위해 컨트롤러, 서비스, 포트, 어댑터, 레포지토리, 도메인 타입, 예외 코드, DTO, 통합 테스트가 대거 신설 및 확장되었습니다. 커서 기반 페이지네이션과 입력값 검증 로직도 포함됩니다.

Changes

파일/그룹 변경 요약
예외 코드
src/main/java/konkuk/thip/common/exception/code/ErrorCode.java
MY ROOM 타입/커서 관련 신규 에러코드 2종 추가 및 기존 코드 번호 조정
컨트롤러
src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java
/rooms/my GET API 및 의존성, 파라미터, 응답 추가
응답 DTO
src/main/java/konkuk/thip/room/adapter/in/web/response/RoomShowMineResponse.java
유저별 방 목록/커서 응답 record 신설
JPA 엔티티
src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java
멤버수 갱신용 public setter 추가
퍼시스턴스 어댑터
src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java
유저 참여방 커서 기반 조회 메서드 4종 추가
레포지토리 인터페이스
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepository.java
유저 참여방 커서 기반 조회 메서드 4종 인터페이스 추가
레포지토리 구현체
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java
커서 기반 참여방 쿼리 4종 및 sliceQuery 로직 구현, participant 네이밍 통일
유스케이스 인터페이스
src/main/java/konkuk/thip/room/application/port/in/RoomShowMineUseCase.java
참여방 조회 유스케이스 인터페이스 신설
포트 인터페이스
src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java
커서 기반 참여방 조회 메서드 4종 인터페이스 추가
커서 슬라이스 DTO
src/main/java/konkuk/thip/room/application/port/out/dto/CursorSliceOfMyRoomView.java
커서/슬라이스 응답 DTO 신설
서비스 구현체
src/main/java/konkuk/thip/room/application/service/RoomShowMineService.java
참여방 조회 서비스 및 입력값 검증, 타입 분기, 커서 페이지네이션 구현
도메인 타입
src/main/java/konkuk/thip/room/domain/MyRoomType.java
참여방 타입 enum 및 from(String) 변환 메서드 신설
통합 테스트
src/test/java/konkuk/thip/room/adapter/in/web/RoomShowMineApiTest.java
참여방 조회 API 통합 테스트(정상/예외/페이지네이션/정렬 등) 신설
커서 유틸
src/main/java/konkuk/thip/common/util/Cursor.java
LocalDate 변환 메서드 추가
매퍼
src/main/java/konkuk/thip/room/application/mapper/RoomQueryMapper.java
DTO → 응답 매핑용 MapStruct 매퍼 신설
쿼리 DTO
src/main/java/konkuk/thip/room/application/port/out/dto/RoomShowMineQueryDto.java
참여방 조회용 QueryDSL 프로젝션 DTO 신설

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller as RoomQueryController
    participant Service as RoomShowMineService
    participant Port as RoomQueryPort
    participant Repo as RoomQueryRepository

    Client->>Controller: GET /rooms/my (userId, type, cursorDate, cursorId)
    Controller->>Service: getMyRooms(userId, type, cursorDate, cursorId)
    Service->>Port: find*RoomsUserParticipated(userId, cursorDate, cursorId, pageSize)
    Port->>Repo: find*RoomsUserParticipated(...)
    Repo-->>Port: CursorSliceOfMyRoomView<MyRoom>
    Port-->>Service: CursorSliceOfMyRoomView<MyRoom>
    Service-->>Controller: RoomShowMineResponse
    Controller-->>Client: BaseResponse<RoomShowMineResponse>
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Assessment against linked issues

Objective Addressed Explanation
유저별 참여한 모임방을 상태별(진행중, 모집중, 완료된)로 조회하는 API 개발 (#89)

Assessment against linked issues: Out-of-scope changes

No out-of-scope changes detected.

Suggested labels

🐶 희진

Poem

🐇
방방곡곡 뛰노는 토끼,
내 방 찾는 API에 신이 났지!
커서로 슝슝, 타입별로 쏙쏙,
테스트도 꼼꼼, 예외도 똑똑.
이제 유저방 찾기,
한결 더 똑부러지지!
🏠✨
"""

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.


📜 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 49ff254 and 8da1411.

📒 Files selected for processing (1)
  • src/main/java/konkuk/thip/room/application/service/RoomShowMineService.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/konkuk/thip/room/application/service/RoomShowMineService.java
⏰ 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
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#89-get-room-mine

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.

❤️ Share
🪧 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 generate unit tests to generate unit tests for 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: 2

🧹 Nitpick comments (13)
src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java (1)

72-76: 멤버 카운트 setter 공개는 과도합니다

테스트 편의를 위한 메서드라면 public 대신 @VisibleForTesting(guava) 사용‧패키지 프라이빗으로 낮추거나 테스트 전용 Factory/Builder를 활용해 실제 코드 경로에서 오용되지 않도록 하는 편이 안전합니다.
또한 음수 값 방지를 위한 검사도 추가하는 것을 권장드립니다.

src/main/java/konkuk/thip/room/application/port/in/RoomShowMineUseCase.java (1)

7-10: 파라미터 객체화 고려

프리미티브 4개가 노출돼 있어 향후 요구사항 추가 시 메서드 시그니처가 잦은 변동을 겪을 수 있습니다.
RoomShowMineQuery 같은 DTO로 묶으면 확장성과 테스트 가독성이 개선됩니다.

src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java (1)

84-92: cursorDate 파싱 포맷 명시 추천

LocalDate 매핑은 ISO 형식이 기본이나, 명시적으로 지정하지 않으면 버전/지역 설정에 따라 실패할 위험이 있습니다.

@RequestParam(value = "cursorDate", required = false)
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate cursorDate

추가로 Swagger 문서에도 포맷 예시(yyyy-MM-dd)를 표기해 주세요.

src/main/java/konkuk/thip/room/adapter/in/web/response/RoomShowMineResponse.java (2)

8-9: 주석 오탈자 수정 제안

현제현재로 수정하면 가독성이 좋아집니다.


6-12: size 필드 중복 위험

size 값은 roomList.size()로 파생 가능합니다. 별도 필드를 유지하면 실제 리스트 크기와 불일치할 여지가 생기니 제거 또는 런타임 계산을 고려해보세요.

src/main/java/konkuk/thip/room/application/service/RoomShowMineService.java (2)

28-31: XOR 검증 가독성 향상

cursorDate == null ^ cursorId == null 표현은 한눈에 이해하기 어렵습니다.
아래처럼 괄호를 추가하거나 명시적 조건으로 풀어쓰면 가독성이 높아집니다.

-if (cursorDate == null ^ cursorId == null) {
+if ((cursorDate == null) ^ (cursorId == null)) {

또는

boolean onlyOneNull = (cursorDate == null) != (cursorId == null);
if (onlyOneNull) { ... }

21-22: 페이지 사이즈 하드코딩

PAGE_SIZE = 10은 요구사항 변동 시 서비스 재배포가 필요합니다.
프로퍼티(예: application.yml)로 빼두면 운영 중 조정이 수월합니다.

src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepository.java (1)

23-30: 메서드 시그니처 네이밍 일관성

lastDate, lastRoomStartDateCursor, dateCursor 등이 혼용되고 있습니다.
통일된 네이밍(예: cursorDate, cursorId)을 사용하면 코드 검색과 유지보수가 쉬워집니다.

src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java (1)

25-31: Port·Repository 네이밍 동기화 필요

앞선 Repository 인터페이스와 동일하게, 포트에서도 파라미터 이름·순서를 일치시키는 편이 좋습니다.
API 설계 관점에서 작은 불일치가 추후 구현체 작성 시 혼란을 줄 수 있습니다.

src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java (2)

297-331: 복합 조회 로직이 잘 구현되었습니다!

진행중인 방과 모집중인 방을 우선순위에 따라 조회하는 로직이 적절합니다. 다만, 복잡한 로직이므로 주석을 더 추가하면 좋겠습니다.


390-399: 쿼리 성능 최적화를 고려하세요

현재 쿼리는 participant, room, book 테이블을 조인하고 있습니다. 대량의 데이터에서 성능을 보장하기 위해 다음 인덱스가 필요합니다:

  • room_participant 테이블의 (user_id, room_id) 복합 인덱스
  • room 테이블의 (start_date, room_id)(end_date, room_id) 복합 인덱스
src/test/java/konkuk/thip/room/adapter/in/web/RoomShowMineApiTest.java (2)

107-117: 멤버 카운트 업데이트 로직 중복

saveSingleUserToRoom 메서드에서 멤버 카운트를 업데이트하는데, 이미 changeRoomMemberCount 메서드가 있습니다.

다음과 같이 리팩토링하는 것을 제안합니다:

 private void saveSingleUserToRoom(RoomJpaEntity roomJpaEntity, UserJpaEntity userJpaEntity) {
     RoomParticipantJpaEntity roomParticipantJpaEntity = RoomParticipantJpaEntity.builder()
             .userJpaEntity(userJpaEntity)
             .roomJpaEntity(roomJpaEntity)
             .roomParticipantRole(RoomParticipantRole.MEMBER)
             .build();
     roomParticipantJpaRepository.save(roomParticipantJpaEntity);
 
-    roomJpaEntity.updateMemberCount(roomJpaEntity.getMemberCount() + 1);
-    roomJpaRepository.save(roomJpaEntity);
+    changeRoomMemberCount(roomJpaEntity, roomJpaEntity.getMemberCount() + 1);
 }

389-442: 테스트 데이터 생성 로직 개선 필요

12개의 방을 생성하는 코드가 매우 반복적입니다. 가독성과 유지보수성을 위해 개선이 필요합니다.

다음과 같은 방식으로 개선할 수 있습니다:

// 테스트 데이터 생성을 위한 헬퍼 메서드
private List<RoomJpaEntity> createMultipleRecruitingRooms(int count, UserJpaEntity user) {
    List<RoomJpaEntity> rooms = new ArrayList<>();
    for (int i = 1; i <= count; i++) {
        RoomJpaEntity room = saveScienceRoom(
            "모집중인방-책-" + i, 
            "isbn" + i, 
            "과학-방-" + i + "일뒤-활동시작", 
            LocalDate.now().plusDays(i), 
            LocalDate.now().plusDays(30), 
            10
        );
        changeRoomMemberCount(room, 5 + (i % 4)); // 다양한 멤버 수
        saveSingleUserToRoom(room, user);
        rooms.add(room);
    }
    return rooms;
}
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between cb2f1a4 and 21d31ca.

📒 Files selected for processing (13)
  • src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (1 hunks)
  • src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java (4 hunks)
  • src/main/java/konkuk/thip/room/adapter/in/web/response/RoomShowMineResponse.java (1 hunks)
  • src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.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/repository/RoomQueryRepository.java (2 hunks)
  • src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java (10 hunks)
  • src/main/java/konkuk/thip/room/application/port/in/RoomShowMineUseCase.java (1 hunks)
  • src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java (2 hunks)
  • src/main/java/konkuk/thip/room/application/port/out/dto/CursorSliceOfMyRoomView.java (1 hunks)
  • src/main/java/konkuk/thip/room/application/service/RoomShowMineService.java (1 hunks)
  • src/main/java/konkuk/thip/room/domain/MyRoomType.java (1 hunks)
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomShowMineApiTest.java (1 hunks)
🧰 Additional context used
🧠 Learnings (6)
src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java (2)

Learnt from: buzz0331
PR: #78
File: src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java:3-3
Timestamp: 2025-07-14T18:22:56.538Z
Learning: THIP 프로젝트에서는 Query API(조회 API)에 한해서는 application 계층에서 adapter.in.web.response 패키지의 response DTO를 직접 참조하는 것을 허용함. 이는 CQRS 아키텍처에서 읽기 전용 작업의 효율성을 위한 팀 컨벤션임.

Learnt from: hd0rable
PR: #57
File: src/test/java/konkuk/thip/room/domain/RoomTest.java:0-0
Timestamp: 2025-07-08T16:30:33.771Z
Learning: Room 도메인에서 startDate는 현재 날짜 이후여야 하는 도메인 규칙이 있어서, 테스트에서 만료된 상태를 시뮬레이션하려면 reflection을 사용해야 한다.

src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepository.java (2)

Learnt from: seongjunnoh
PR: #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: buzz0331
PR: #78
File: src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java:3-3
Timestamp: 2025-07-14T18:22:56.538Z
Learning: THIP 프로젝트에서는 Query API(조회 API)에 한해서는 application 계층에서 adapter.in.web.response 패키지의 response DTO를 직접 참조하는 것을 허용함. 이는 CQRS 아키텍처에서 읽기 전용 작업의 효율성을 위한 팀 컨벤션임.

src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java (2)

Learnt from: seongjunnoh
PR: #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: hd0rable
PR: #57
File: src/test/java/konkuk/thip/room/domain/RoomTest.java:0-0
Timestamp: 2025-07-08T16:30:33.771Z
Learning: Room 도메인에서 startDate는 현재 날짜 이후여야 하는 도메인 규칙이 있어서, 테스트에서 만료된 상태를 시뮬레이션하려면 reflection을 사용해야 한다.

src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java (1)

Learnt from: seongjunnoh
PR: #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/RoomShowMineApiTest.java (1)

Learnt from: hd0rable
PR: #57
File: src/test/java/konkuk/thip/room/domain/RoomTest.java:0-0
Timestamp: 2025-07-08T16:30:33.771Z
Learning: Room 도메인에서 startDate는 현재 날짜 이후여야 하는 도메인 규칙이 있어서, 테스트에서 만료된 상태를 시뮬레이션하려면 reflection을 사용해야 한다.

src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java (2)

Learnt from: seongjunnoh
PR: #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: buzz0331
PR: #78
File: src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java:3-3
Timestamp: 2025-07-14T18:22:56.538Z
Learning: THIP 프로젝트에서는 Query API(조회 API)에 한해서는 application 계층에서 adapter.in.web.response 패키지의 response DTO를 직접 참조하는 것을 허용함. 이는 CQRS 아키텍처에서 읽기 전용 작업의 효율성을 위한 팀 컨벤션임.

🧬 Code Graph Analysis (1)
src/test/java/konkuk/thip/room/adapter/in/web/RoomShowMineApiTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
  • TestEntityFactory (21-172)
⏰ 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 (10)
src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (1)

120-122: 새 오류 코드 추가 👍

도메인 예외 분리가 깔끔하며 코드 값 충돌도 없습니다.
추가 검증 로직과 매핑만 누락되지 않았는지 확인해 주세요.

src/main/java/konkuk/thip/room/application/port/out/dto/CursorSliceOfMyRoomView.java (1)

10-21: 문제 없음 – 구현 적합

커서 정보를 포함한 Slice 확장은 깔끔하며, 별다른 결함이 보이지 않습니다.

src/main/java/konkuk/thip/room/adapter/in/web/response/RoomShowMineResponse.java (1)

14-19: endDate 타입 신중 검토

endDateString으로 두면 날짜 연산이 어려워집니다.
가능하다면 LocalDate/LocalDateTime으로 유지하고, 포맷은 컨트롤러에서 처리하도록 고민해보세요.

src/main/java/konkuk/thip/room/application/service/RoomShowMineService.java (1)

46-52: size 중복 계산

RoomShowMineResponsesize를 전달하면서 slice.getContent().size()를 다시 넘깁니다.
응답 DTO에서 size 필드를 제거하거나, 생성자 내부에서 list 크기를 계산하도록 일관성을 맞춰주세요.

src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java (2)

47-50: 구현이 적절합니다!

어댑터 패턴에 맞게 리포지토리로 위임하는 구조가 잘 구현되었습니다.


52-66: 나머지 메서드들도 일관성 있게 구현되었습니다!

모든 메서드가 동일한 패턴으로 리포지토리에 위임하고 있어 구조적 일관성이 유지되고 있습니다.

src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java (2)

41-41: 별칭 변경이 적절합니다!

participantuserRoom보다 엔티티의 의미를 더 명확하게 표현합니다.


252-272: 모집중인 방 조회 로직이 올바르게 구현되었습니다!

필터링 조건과 정렬 기준이 요구사항에 맞게 잘 구현되었습니다.

src/test/java/konkuk/thip/room/adapter/in/web/RoomShowMineApiTest.java (2)

63-71: 테스트 정리가 적절합니다!

@AfterEach에서 역순으로 데이터를 삭제하는 것이 참조 무결성을 고려한 좋은 구현입니다.


120-156: 테스트 검증 로직이 명확합니다!

각 시나리오별로 예상 결과를 명확하게 검증하고 있으며, 정렬 순서와 필터링이 올바르게 동작하는지 확인하고 있습니다.

Comment on lines +20 to +27
public static MyRoomType from(String type) {
return Arrays.stream(MyRoomType.values())
.filter(param -> param.getType().equals(type))
.findFirst()
.orElseThrow(
() -> new InvalidStateException(ErrorCode.INVALID_MY_ROOM_TYPE)
);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

대소문자·null 안전성 보강 필요

from()equals로만 비교해 "Playing"·PLAYING 등은 거부하며, null 입력 시 NPE가 발생합니다.

 public static MyRoomType from(String type) {
-    return Arrays.stream(values())
-            .filter(t -> t.type.equals(type))
+    if (type == null) throw new InvalidStateException(ErrorCode.INVALID_MY_ROOM_TYPE);
+    return Arrays.stream(values())
+            .filter(t -> t.type.equalsIgnoreCase(type))
             .findFirst()
             .orElseThrow(() -> new InvalidStateException(ErrorCode.INVALID_MY_ROOM_TYPE));
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/room/domain/MyRoomType.java around lines 20 to 27,
the from() method currently uses equals for comparison, which is case-sensitive
and throws a NullPointerException if the input is null. Modify the method to
first check if the input string is null and throw an appropriate exception or
handle it safely. Then, perform a case-insensitive comparison (e.g., using
equalsIgnoreCase) to allow matching regardless of letter case.

- 클라이언트와 주고받는 커서는 현재 조회할 페이지의 첫번째 레코드의 정보를 의미하므로, 변수 네이밍도 의미를 잘 나타내도록 수정
hd0rable
hd0rable previously approved these changes Jul 21, 2025
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.

수고하셨어요!! 복잡한 조회로직 살벌하네요,,😱😱

@GetMapping("/rooms/my")
public BaseResponse<RoomShowMineResponse> getMyRooms(
@UserId final Long userId,
@RequestParam(value = "type", required = false, defaultValue = "playingAndRecruiting") final String type,
Copy link
Member

Choose a reason for hiding this comment

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

LGTM

}
} No newline at end of file

// 테스트 메서드 편의용
Copy link
Member

Choose a reason for hiding this comment

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

p3: 이건 나즁에,, 리펙토링하는게 좋아보여욥

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@buzz0331 @hd0rable 저희가 RoomJpaEntity 에 memberCount 값을 추가하여서 기존 테스트 코드 작성시 특정 방에 어떤 유저가 속해있다 라는 상황을 가정할때 더이상 RoomParticipantJpaEntity 를 DB에 save 하지 않고, room의 memberCount 값을 수정함으로써 테스트 코드를 간결하게 작성할 수 있는 이점이 있지 않을까 생각하긴 합니다

이런 이유로 RoomJpaEntity에 updateMemberCount 라는 메서드를 추가하였고, jpa entity 는 외부로 노출되는 메서드가 아니니 테스트 메서드 편의용으로 괜찮지 않나라는 생각이긴 합니다

관련해서 어떻게 생각하시나요?

Copy link
Member

Choose a reason for hiding this comment

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

updateMemberCount()를 통해 테스트 코드가 간결해질 수 있다는 점에는 공감합니다. 또한 테스트를 준비하는 데 필요한 반복적인 작업들을 줄일 수 있다는 이점도 분명하다고 생각했습니다.

다만 JPA 엔티티는 도메인-DB 매핑과 정합성을 책임지는 계층이기 때문에,
테스트를 위한 조작용 메서드를 엔티티 내부에 추가하는 방식은 설계적으로 다소 책임이 분산될 수 있다고 느꼈습니다.
가능하다면 테스트 전용 팩토리나 빌더 객체를 활용해서 memberCount를 설정하거나,
RoomParticipant를 실제로 생성해서 상태를 만드는 쪽이 더 명확한 구조가 될 것 같습니다.

그래도 간편성 차원에선 고려 해볼법한 방법이라 생각하며, 유지한다면 명확히 @VisibleForTesting 또는 JavaDoc 주석을 명시해두는 것도 좋을 것 같습니다.

Copy link
Collaborator Author

@seongjunnoh seongjunnoh Jul 22, 2025

Choose a reason for hiding this comment

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

@VisibleForTesting 어노테이션을 통한 문서화도 좋은 방법인것 같습니다!!

@buzz0331 현준님 생각은 어떠신가요?

Copy link
Contributor

Choose a reason for hiding this comment

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

테스트 편의를 위해 update 메서드 뚫어놓는 것은 좋은 것 같아요! 특정 테스트 환경을 만들기 위해서는 불가피할 것 같습니다.

저도 저번에 방 참여 api의 테스트코드를 작성하기 위해서 @VisibleForTesting을 도입해서 updateStartDate 메서드를 구현해두었는데 테스트에서도 접근이 가능하고, 서비스 코드에서도 접근이 가능하더라구요,, @VisibleForTesting이 하는 역할이 정확히 어떤건지 아시나요??

Copy link
Member

Choose a reason for hiding this comment

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

@VisibleForTesting도 사실상 주석처럼 "테스트에서만 사용하겠다"라는걸 명시하는 어노테이션으로 사용한다고 하네요

Copy link
Contributor

Choose a reason for hiding this comment

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

아하 접근 제한자를 실제로 변경해주는 것은 아니고 주석 정도의 역할이군요! 주석을 사용하는 것보다 조금더 명시적일 것 같아서 좋을 것 같네요!!


CursorSliceOfMyRoomView<RoomShowMineResponse.MyRoom> findPlayingAndRecruitingRoomsUserParticipated(Long userId, LocalDate dateCursor, Long roomIdCursor, int pageSize);

CursorSliceOfMyRoomView<RoomShowMineResponse.MyRoom> findExpiredRoomsUserParticipated(Long userId, LocalDate dateCursor, Long roomIdCursor, int pageSize);
Copy link
Member

Choose a reason for hiding this comment

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

😱수고하셨어요,,

private final QBookJpaEntity book = QBookJpaEntity.bookJpaEntity;
private final QRoomParticipantJpaEntity userRoom = QRoomParticipantJpaEntity.roomParticipantJpaEntity;

private final QRoomParticipantJpaEntity participant = QRoomParticipantJpaEntity.roomParticipantJpaEntity;
Copy link
Member

Choose a reason for hiding this comment

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

LGTM

/**
* 공통 커서+페이징 처리
*/
private CursorSliceOfMyRoomView<RoomShowMineResponse.MyRoom> sliceQuery(
Copy link
Member

Choose a reason for hiding this comment

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

LGTM 🤩

OrderSpecifier<?>[] orders = new OrderSpecifier<?>[]{
cursorExpr.asc(), room.roomId.asc()
};
Function<Tuple,RoomShowMineResponse.MyRoom> mapper = t -> new RoomShowMineResponse.MyRoom( // tuple -> DTO 매핑 함수
Copy link
Member

Choose a reason for hiding this comment

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

이함수도 매핑하는 함수로 중복 코드가 많은것같아서 나중에 리펙하면 좋을것같아욥

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

오 예리하시네요
해당 response 로 mapping 하는 메서드 분리하는 식으로 수정하겠습니다!

buzz0331
buzz0331 previously approved these changes Jul 21, 2025
Copy link
Contributor

@buzz0331 buzz0331 left a comment

Choose a reason for hiding this comment

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

수고하셨습니다~~

Comment on lines 356 to 423
/**
* 공통 커서+페이징 처리
*/
private CursorSliceOfMyRoomView<RoomShowMineResponse.MyRoom> sliceQuery(
BooleanExpression baseCondition,
DateExpression<LocalDate> cursorDateExpr,
Function<Tuple,RoomShowMineResponse.MyRoom> mapper,
LocalDate dateCursor,
Long roomIdCursor,
int pageSize,
boolean ascending,
OrderSpecifier<?>... orderSpecs
) {
BooleanBuilder where = new BooleanBuilder(baseCondition); // baseCondition + 커서 기반으로 where 절 구성
if (dateCursor != null && roomIdCursor != null) {
if (ascending) { // 진행중, 모집중, 통합
where.and(
cursorDateExpr.gt(dateCursor) // dateCursor 보다 크거나
.or(
cursorDateExpr.eq(dateCursor)
.and(room.roomId.goe(roomIdCursor)) // 같으면 id가 roomIdCursor 보다 크거나 같은 것
)
);
} else { // 내림차순일 때는 반대로 (만료된 방)
where.and(
cursorDateExpr.lt(dateCursor) // dateCursor 보다 작거나
.or(
cursorDateExpr.eq(dateCursor)
.and(room.roomId.loe(roomIdCursor)) // 같으면 id가 roomIdCursor 보다 작거나 같은 것
)
);
}
}

int fetchSize = pageSize + 1;
List<Tuple> tuples = queryFactory
.select(room.roomId, book.imageUrl, room.title, room.memberCount, cursorDateExpr)
.from(participant)
.join(participant.roomJpaEntity, room)
.join(room.bookJpaEntity, book)
.where(where)
.orderBy(orderSpecs)
.limit(fetchSize)
.fetch(); // 직접 tuple 결과를 조회하므로 lazy 로딩 적용 대상 X

boolean hasNext = tuples.size() == fetchSize;
List<RoomShowMineResponse.MyRoom> content = tuples.stream()
.limit(pageSize)
.map(mapper)
.toList(); // pageSize 만큼만 dto로 매핑

// 커서 값 세팅
LocalDate nextDate = null;
Long nextId = null;
if (hasNext) {
Tuple next = tuples.get(pageSize); // 다음 페이지의 첫번째 레코드
nextDate = next.get(cursorDateExpr);
nextId = next.get(room.roomId);
}

return new CursorSliceOfMyRoomView<>(
content,
PageRequest.of(0, pageSize),
hasNext,
nextDate,
nextId
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

현재 페이징 변수를 구하는 로직때문에 쿼리 볼륨이 커진 느낌입니다. 회의에서 말한 것처럼 limit를 1 높게 조회하여 adapter 또는 service에서 CursorBasedList를 만들때, nextCursor를 추출하면 쿼리 볼륨이 조금 줄어들 것 같습니다!

지금은 고치기 어려우니 추후 리팩토링 시에 고치도록 하죠!!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

pageSize + 1 개를 조회하여 next 페이지의 유무를 확인하고 커서값을 세팅하도록 구현하긴 했는데, 아무래도 해당 메서드에서
쿼리 필터 조건 처리 + DB 쿼리 조회 + 다음 페이지 유무 및 커서 생성 의 작업을 모두 수행하다 보니 볼륨이 좀 크긴 하네요

추후에 복합 커서용 CursorBasedList 같은 친구를 도입하는 식으로 리펙헤보겠습니다!

Copy link
Contributor

Choose a reason for hiding this comment

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

굿굿!

Comment on lines +468 to +524
@Test
@DisplayName("cursor 값을 기준으로 해당 페이지의 데이터를 반환한다.")
void get_my_rooms_page_2() throws Exception {
//given
RoomJpaEntity recruitingRoom1 = saveScienceRoom("모집중인방-책-1", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), LocalDate.now().plusDays(30), 10);
changeRoomMemberCount(recruitingRoom1, 5);

RoomJpaEntity recruitingRoom2 = saveScienceRoom("모집중인방-책-2", "isbn2", "과학-방-2일뒤-활동시작", LocalDate.now().plusDays(2), LocalDate.now().plusDays(30), 10);
changeRoomMemberCount(recruitingRoom2, 8);

RoomJpaEntity recruitingRoom3 = saveScienceRoom("모집중인방-책-3", "isbn3", "과학-방-3일뒤-활동시작", LocalDate.now().plusDays(3), LocalDate.now().plusDays(30), 10);
changeRoomMemberCount(recruitingRoom3, 8);

RoomJpaEntity recruitingRoom4 = saveScienceRoom("모집중인방-책-4", "isbn4", "과학-방-4일뒤-활동시작", LocalDate.now().plusDays(4), LocalDate.now().plusDays(30), 10);
changeRoomMemberCount(recruitingRoom4, 8);

RoomJpaEntity recruitingRoom5 = saveScienceRoom("모집중인방-책-5", "isbn5", "과학-방-5일뒤-활동시작", LocalDate.now().plusDays(5), LocalDate.now().plusDays(30), 10);
changeRoomMemberCount(recruitingRoom5, 8);

RoomJpaEntity recruitingRoom6 = saveScienceRoom("모집중인방-책-6", "isbn6", "과학-방-6일뒤-활동시작", LocalDate.now().plusDays(6), LocalDate.now().plusDays(30), 10);
changeRoomMemberCount(recruitingRoom6, 8);

RoomJpaEntity recruitingRoom7 = saveScienceRoom("모집중인방-책-7", "isbn7", "과학-방-7일뒤-활동시작", LocalDate.now().plusDays(7), LocalDate.now().plusDays(30), 10);
changeRoomMemberCount(recruitingRoom7, 8);

RoomJpaEntity recruitingRoom8 = saveScienceRoom("모집중인방-책-8", "isbn8", "과학-방-8일뒤-활동시작", LocalDate.now().plusDays(8), LocalDate.now().plusDays(30), 10);
changeRoomMemberCount(recruitingRoom8, 8);

RoomJpaEntity recruitingRoom9 = saveScienceRoom("모집중인방-책-9", "isbn9", "과학-방-9일뒤-활동시작", LocalDate.now().plusDays(9), LocalDate.now().plusDays(30), 10);
changeRoomMemberCount(recruitingRoom9, 8);

RoomJpaEntity recruitingRoom10 = saveScienceRoom("모집중인방-책-10", "isbn10", "과학-방-10일뒤-활동시작", LocalDate.now().plusDays(10), LocalDate.now().plusDays(30), 10);
changeRoomMemberCount(recruitingRoom10, 8);

RoomJpaEntity recruitingRoom11 = saveScienceRoom("모집중인방-책-11", "isbn11", "과학-방-11일뒤-활동시작", LocalDate.now().plusDays(11), LocalDate.now().plusDays(30), 10);
changeRoomMemberCount(recruitingRoom11, 8);

RoomJpaEntity recruitingRoom12 = saveScienceRoom("모집중인방-책-12", "isbn12", "과학-방-12일뒤-활동시작", LocalDate.now().plusDays(12), LocalDate.now().plusDays(30), 10);
changeRoomMemberCount(recruitingRoom12, 8);

AliasJpaEntity scienceAlias = aliasJpaRepository.save(TestEntityFactory.createScienceAlias());
UserJpaEntity user = userJpaRepository.save(TestEntityFactory.createUser(scienceAlias));

// user가 생성한 방에 참여한 상황 가정
saveSingleUserToRoom(recruitingRoom1, user);
saveSingleUserToRoom(recruitingRoom2, user);
saveSingleUserToRoom(recruitingRoom3, user);
saveSingleUserToRoom(recruitingRoom4, user);
saveSingleUserToRoom(recruitingRoom5, user);
saveSingleUserToRoom(recruitingRoom6, user);
saveSingleUserToRoom(recruitingRoom7, user);
saveSingleUserToRoom(recruitingRoom8, user);
saveSingleUserToRoom(recruitingRoom9, user);
saveSingleUserToRoom(recruitingRoom10, user);
saveSingleUserToRoom(recruitingRoom11, user);
saveSingleUserToRoom(recruitingRoom12, user);

Copy link
Contributor

Choose a reason for hiding this comment

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

ㅋㅋㅋㅋㅋㅋㅋㅋ살짝 광기가 느껴지네여

Comment on lines 34 to 43
CursorSliceOfMyRoomView<RoomShowMineResponse.MyRoom> slice = switch (MyRoomType.from(type)) {
case RECRUITING -> roomQueryPort
.findRecruitingRoomsUserParticipated(userId, cursorDate, cursorId, PAGE_SIZE);
case PLAYING -> roomQueryPort
.findPlayingRoomsUserParticipated(userId, cursorDate, cursorId, PAGE_SIZE);
case PLAYING_AND_RECRUITING -> roomQueryPort
.findPlayingAndRecruitingRoomsUserParticipated(userId, cursorDate, cursorId, PAGE_SIZE);
case EXPIRED -> roomQueryPort
.findExpiredRoomsUserParticipated(userId, cursorDate, cursorId, PAGE_SIZE);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

LGTM

@seongjunnoh seongjunnoh dismissed stale reviews from buzz0331 and hd0rable via 7f40f4c July 22, 2025 14:45
Copy link
Collaborator Author

@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.

@buzz0331 @hd0rable 페이징 처리 관련해서 수정한 후 머지하겠습니다.

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: 1

🧹 Nitpick comments (4)
src/main/java/konkuk/thip/room/application/port/in/RoomShowMineUseCase.java (1)

7-7: 타입 안정성을 위해 도메인 타입 사용을 고려해보세요

typecursor 파라미터가 String으로 정의되어 있어 런타임에 검증이 필요합니다. MyRoomType enum과 Cursor 객체를 직접 파라미터로 받는 것이 타입 안정성 측면에서 더 나을 수 있습니다.

-    RoomShowMineResponse getMyRooms(Long userId, String type, String cursor);
+    RoomShowMineResponse getMyRooms(Long userId, MyRoomType type, Cursor cursor);
src/main/java/konkuk/thip/room/application/service/RoomShowMineService.java (1)

45-61: EXPIRED 타입 처리를 Mapper에서 수행하는 것을 고려해보세요

현재 EXPIRED 타입에 대한 특별 처리가 서비스 레이어에서 이루어지고 있습니다. 이를 RoomQueryMapper에 위임하면 서비스 로직이 더 간결해질 수 있습니다.

-        boolean isExpiredType = myRoomType == MyRoomType.EXPIRED;
         List<RoomShowMineResponse.MyRoom> myRooms = result.contents().stream()
-                .map(dto -> {
-                    RoomShowMineResponse.MyRoom r = roomQueryMapper.toShowMyRoomResponse(dto);
-                    if (isExpiredType) {
-                        return new RoomShowMineResponse.MyRoom(
-                                r.roomId(),
-                                r.bookImageUrl(),
-                                r.roomName(),
-                                r.memberCount(),
-                                null
-                        );
-                    }
-                    return r;
-                })
+                .map(dto -> roomQueryMapper.toShowMyRoomResponse(dto, myRoomType))
                 .toList();

Mapper 인터페이스에 다음 메서드를 추가:

default RoomShowMineResponse.MyRoom toShowMyRoomResponse(RoomShowMineQueryDto dto, MyRoomType type) {
    RoomShowMineResponse.MyRoom room = toShowMyRoomResponse(dto);
    if (type == MyRoomType.EXPIRED) {
        return new RoomShowMineResponse.MyRoom(
            room.roomId(),
            room.bookImageUrl(),
            room.roomName(),
            room.memberCount(),
            null
        );
    }
    return room;
}
src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java (1)

26-32: 단일 메서드로 통합하는 것을 고려해보세요

현재 4개의 별도 메서드로 구현되어 있는데, 타입을 파라미터로 받는 단일 메서드로 통합하면 인터페이스가 더 간결해질 수 있습니다.

-    CursorBasedList<RoomShowMineQueryDto> findRecruitingRoomsUserParticipated(Long userId, Cursor cursor);
-
-    CursorBasedList<RoomShowMineQueryDto> findPlayingRoomsUserParticipated(Long userId, Cursor cursor);
-
-    CursorBasedList<RoomShowMineQueryDto> findPlayingAndRecruitingRoomsUserParticipated(Long userId, Cursor cursor);
-
-    CursorBasedList<RoomShowMineQueryDto> findExpiredRoomsUserParticipated(Long userId, Cursor cursor);
+    CursorBasedList<RoomShowMineQueryDto> findRoomsUserParticipatedByType(Long userId, MyRoomType type, Cursor cursor);

다만, 현재 구현이 각 타입별로 다른 쿼리 로직을 가지고 있다면 현재 방식이 더 적절할 수 있습니다.

src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java (1)

50-51: Cursor 파라미터 추출 로직을 Cursor 클래스로 이동 고려

커서에서 파라미터를 추출하는 로직이 반복되고 있습니다. 이를 Cursor 클래스의 메서드로 캡슐화하면 더 깔끔해질 수 있습니다.

// Cursor 클래스에 추가
public record CursorParams(LocalDate date, Long id) {
    public static CursorParams from(Cursor cursor) {
        if (cursor.isFirstRequest()) {
            return new CursorParams(null, null);
        }
        return new CursorParams(cursor.getLocalDate(0), cursor.getLong(1));
    }
}

그러면 이렇게 사용할 수 있습니다:

-        LocalDate lastLocalDate = cursor.isFirstRequest() ? null : cursor.getLocalDate(0);
-        Long lastId = cursor.isFirstRequest() ? null : cursor.getLong(1);
+        CursorParams params = CursorParams.from(cursor);
         int pageSize = cursor.getPageSize();
         
-        List<RoomShowMineQueryDto> roomShowMineQueryDtos = roomJpaRepository.findRecruitingRoomsUserParticipated(userId, lastLocalDate, lastId, pageSize);
+        List<RoomShowMineQueryDto> roomShowMineQueryDtos = roomJpaRepository.findRecruitingRoomsUserParticipated(userId, params.date(), params.id(), pageSize);

Also applies to: 67-68, 84-85, 101-102

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 7f40f4c and 3d39ed4.

📒 Files selected for processing (13)
  • src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (1 hunks)
  • src/main/java/konkuk/thip/common/util/Cursor.java (2 hunks)
  • src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java (4 hunks)
  • src/main/java/konkuk/thip/room/adapter/in/web/response/RoomShowMineResponse.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/repository/RoomQueryRepository.java (2 hunks)
  • src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java (9 hunks)
  • src/main/java/konkuk/thip/room/application/mapper/RoomQueryMapper.java (1 hunks)
  • src/main/java/konkuk/thip/room/application/port/in/RoomShowMineUseCase.java (1 hunks)
  • src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java (2 hunks)
  • src/main/java/konkuk/thip/room/application/port/out/dto/RoomShowMineQueryDto.java (1 hunks)
  • src/main/java/konkuk/thip/room/application/service/RoomShowMineService.java (1 hunks)
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomShowMineApiTest.java (1 hunks)
✅ Files skipped from review due to trivial changes (3)
  • src/main/java/konkuk/thip/room/application/mapper/RoomQueryMapper.java
  • src/main/java/konkuk/thip/common/util/Cursor.java
  • src/main/java/konkuk/thip/room/application/port/out/dto/RoomShowMineQueryDto.java
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java
  • src/main/java/konkuk/thip/common/exception/code/ErrorCode.java
  • src/main/java/konkuk/thip/room/adapter/in/web/response/RoomShowMineResponse.java
  • src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomShowMineApiTest.java
  • src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepository.java
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#101
File: src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java:36-39
Timestamp: 2025-07-26T06:09:00.850Z
Learning: THIP 프로젝트에서는 "사용자가 방에 속하는지 검증" 로직을 RoomParticipantPolicy 도메인 서비스로 캡슐화하여 재사용성을 높이고 비즈니스 로직의 중복을 방지하는 방식을 선호한다.
src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java (1)

Learnt from: seongjunnoh
PR: #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: #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/service/RoomShowMineService.java (1)

Learnt from: seongjunnoh
PR: #101
File: src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java:36-39
Timestamp: 2025-07-26T06:09:00.850Z
Learning: THIP 프로젝트에서는 "사용자가 방에 속하는지 검증" 로직을 RoomParticipantPolicy 도메인 서비스로 캡슐화하여 재사용성을 높이고 비즈니스 로직의 중복을 방지하는 방식을 선호한다.

⏰ 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/main/java/konkuk/thip/room/application/service/RoomShowMineService.java (2)

18-25: 서비스 구현이 깔끔합니다!

의존성 주입과 읽기 전용 트랜잭션 설정이 적절합니다.


34-43: switch 표현식 사용이 우수합니다

Java 14의 switch 표현식을 활용하여 타입별 분기 처리가 깔끔하게 구현되었습니다.

src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java (1)

26-32: 메서드 명명 규칙이 일관되고 명확합니다

사용자가 참여한 방을 상태별로 조회하는 메서드들의 이름이 의도를 명확하게 전달합니다.

Copy link
Collaborator Author

@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.

새로 도입된 CursorBasedList, Cursor 반영하여 api 의 전체적인 코드를 수정하였습니다.

또한 기존에는 request param으로 전달받은 커서의 범위부터 조회하는 방식으로 페이징처리를 구현하였지만, 팀 컨벤션인 exclusive 커서 페이징 방식에 따라 페이징 처리 로직을 수정하였습니다

그리고 조회용 dto 도입 및 QueryDSL 코드에서 Q클래스 dto를 활용하여 조회 및 반환을 한번에 할 수 있도록 수정하였습니다
-> "방과 관련된 조회용 데이터를 담는 dto" 를 정의할까 하다가, 해당 dto에는 현재 api 로직과는 상관없는 데이터들이 많을 것이고, 이로 인한 코드의 수정 과정이 효율적이지 않다고 생각하여 현재 api 전용 조회 dto인 RoomShowMineQueryDto 를 정의하였습니다

Copy link
Contributor

@buzz0331 buzz0331 left a comment

Choose a reason for hiding this comment

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

굿굿입니다!! 소소한 리뷰 하나 남겼는데 확인 부탁드릴게여~ 확실히 CursorBasedList를 이용하니 코드 로직이 좀더 잘 보이는 느낌이네요!!

Comment on lines 49 to 57
RoomShowMineResponse.MyRoom r = roomQueryMapper.toShowMyRoomResponse(dto);
if (isExpiredType) {
return new RoomShowMineResponse.MyRoom(
r.roomId(),
r.bookImageUrl(),
r.roomName(),
r.memberCount(),
null
);
Copy link
Contributor

Choose a reason for hiding this comment

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

p3: r->myRoomDto으로 명시하는 것 어떨까요? 명시하면 RoomShowMineResponse.MyRoom 타입을 사용하지 않고 var로 타입을 바꿔도 어떤 클래스인지 알 수 있을 것 같아요!

var myRoomDto = roomQueryMapper.toShowMyRoomResponse(dto);
                    if (isExpiredType) {
                        return new RoomShowMineResponse.MyRoom(
                                myRoomDto.roomId(),
                               ...
                        );

Comment on lines 33 to 43
MyRoomType myRoomType = MyRoomType.from(type);
CursorBasedList<RoomShowMineQueryDto> result = switch (myRoomType) {
case RECRUITING -> roomQueryPort
.findRecruitingRoomsUserParticipated(userId, cursorDate, cursorId, PAGE_SIZE);
.findRecruitingRoomsUserParticipated(userId, nextCursor);
case PLAYING -> roomQueryPort
.findPlayingRoomsUserParticipated(userId, cursorDate, cursorId, PAGE_SIZE);
.findPlayingRoomsUserParticipated(userId, nextCursor);
case PLAYING_AND_RECRUITING -> roomQueryPort
.findPlayingAndRecruitingRoomsUserParticipated(userId, cursorDate, cursorId, PAGE_SIZE);
.findPlayingAndRecruitingRoomsUserParticipated(userId, nextCursor);
case EXPIRED -> roomQueryPort
.findExpiredRoomsUserParticipated(userId, cursorDate, cursorId, PAGE_SIZE);
.findExpiredRoomsUserParticipated(userId, nextCursor);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

LGTM

Copy link
Contributor

@buzz0331 buzz0331 left a comment

Choose a reason for hiding this comment

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

수고하셨습니다 머지 ㄱㄱ~

));
return nextCursor.toEncodedString();
});
return findRooms(userId, cursor, roomJpaRepository::findRecruitingRoomsUserParticipated);
Copy link
Contributor

Choose a reason for hiding this comment

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

오호 엄청 간단해지네여 👍🏻

@seongjunnoh
Copy link
Collaborator Author

굿굿입니다!! 소소한 리뷰 하나 남겼는데 확인 부탁드릴게여~ 확실히 CursorBasedList를 이용하니 코드 로직이 좀더 잘 보이는 느낌이네요!!

CursorBasedList 도입은 매우 좋은 것 같습니다!!! 덕분에 커서 기반 조회 로직에 통일성이 생겨서 수정 및 리뷰가 편해진거 같아여

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-132] [feat] 유저별 참여한 모임방 조회 api 개발

3 participants