Skip to content

[Refactor] 저장한 책, 참여중인 방의 책 조회시 커서기반 무한스크롤 추가/ [모임 홈] 진행중인 방 조회시 페이지기반 무한스크롤을 커서기반으로 수정#284

Merged
hd0rable merged 38 commits intodevelopfrom
refactor/#283-cursor-book
Aug 30, 2025

Conversation

@hd0rable
Copy link
Member

@hd0rable hd0rable commented Aug 28, 2025

#️⃣ 연관된 이슈

closes #283

📝 작업 내용

  • 모임 홈 화면에서 진행중인 방 조회시 페이지기반 무한스크롤을 커서기반으로 수정했습니다.
  • 저장한 책 조회시 누락되었던 무한스크롤을 추가했습니다.
  • 또한 저장한 책 또는 참여중인 모임 방의 책 조회시 누락되었던 무한스크롤을 추가했습니다.
  • 기존에 저장한 책 또는 참여중인 모임 방의 책 조회시에는 페이징이 필요없어서 Result 패턴을 적용하였는데 페이징 처리가 추가됨에 따라 inner class를 사용하도록 컨벤션에 맞게 수정하였습니다.
  • 해당 Result 패턴적용이 해제되면서 BookSelectableResultBookPinResult로 재사용하도록 했습니다.
  • 저장한 책 조회 시 정렬 순서는 책을 저장한 시간 빠른 순으로 조회되어 단일커서 savedBook.createdAt를 사용했습니다.
  • 참여중인 모임 방 책 조회 시 정렬 순서는 방 진행도 높은 순 -> 책 아이디 낮은순으로 복합커서를 사용했습니다.
  • 모임 홈 화면에서 진행중인 방 조회 시 정렬 순서는 유저 진행도 높은 순-> 방 활동 시작일 빠른 순 -> 방 아이디 작은 순으로 복합커서를 사용했습니다.
  • 관련해서 테스트코드 상세하게 작성했으니 참고해주시면 감사하겠습니다
  • 노션 response도 업데이트 해두었습니다.

📸 스크린샷

image image

💬 리뷰 요구사항

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

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

Summary by CodeRabbit

  • New Features

    • 여러 목록 API(도서 선택/저장, 홈 참여중 방, 즐겨찾기 피드 등)에 커서 기반 페이지네이션과 선택적 cursor 파라미터 도입
  • Enhancements

    • 목록 응답에 nextCursor·isLast 추가로 연속 페이지 지원, 저장 도서는 최신순 정렬, 참여 도서는 진행률 기준 정렬(동률 시 안정적 순서)
    • 응답 항목 구조 정비 및 일부 핀 응답 간소화
    • 커서 유틸 확장(실수형 접근 등)
  • Tests

    • 통합 테스트 보강: 커서 페이징·정렬 시나리오, 생성시각 고정 및 데이터 정리 개선

@coderabbitai
Copy link

coderabbitai bot commented Aug 28, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

여러 도메인에서 페이지 기반 목록 조회를 커서 기반으로 전환하고, 저장한 책의 누락된 무한스크롤을 커서로 추가하는 등 포트/어댑터/서비스/레포지토리/컨트롤러/DTO/매퍼 및 테스트들을 커서·DTO 중심으로 변경했습니다.

Changes

Cohort / File(s) Summary
Web Controllers
src/main/java/konkuk/thip/book/adapter/in/web/BookQueryController.java, src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java, src/main/java/konkuk/thip/roompost/adapter/in/web/RoomPostQueryController.java
엔드포인트에 선택적 cursor 파라미터 추가 및 컨트롤러→서비스 호출에서 커서 전달; RecordPin 엔드포인트 입력/출력 구축방식 변경.
Book Responses / DTOs
src/main/java/konkuk/thip/book/adapter/in/web/response/BookSelectableListResponse.java, .../BookShowSavedListResponse.java, src/main/java/konkuk/thip/book/application/port/in/dto/BookPinResult.java
항목 DTO를 레코드로 재정의하고 응답에 nextCursor·isLast 추가; BookSelectableResult → BookPinResult로 리네이밍 및 필드 조정.
Book Mapper / Services / UseCases
src/main/java/konkuk/thip/book/application/mapper/BookQueryMapper.java, .../BookSavedListService.java, .../BookSelectableListService.java, .../BookShowSavedListUseCase.java, .../BookSelectableListUseCase.java
매핑을 BookQueryDto 기반으로 전환, 서비스·유스케이스 시그니처에 cursor 인자 추가 및 반환 타입을 커서 메타 포함 응답으로 변경.
Book Persistence / Repos
src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java, .../repository/BookJpaRepository.java, .../repository/BookQueryRepository.java, .../repository/BookQueryRepositoryImpl.java, src/main/java/konkuk/thip/book/application/port/out/dto/BookQueryDto.java
기존 List 기반 쿼리 제거 후 QueryDSL projection DTO 도입, 전용 BookQueryRepository 추가·구현, 포트/어댑터가 Cursor 기반 CursorBasedList 사용하도록 변경.
Cursor util
src/main/java/konkuk/thip/common/util/Cursor.java
Cursor에 getDouble(int) 접근자 추가.
Feed naming
src/main/java/konkuk/thip/feed/.../FeedQueryPersistenceAdapter.java, .../FeedQueryPort.java, .../FeedShowSavedListService.java
메서드명 정비: findSavedFeedsByCreatedAtfindSavedFeedsBySavedAt(시그니처 유지).
Room DTOs / Repos / Service / Mapper
src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetHomeJoinedListResponse.java, .../RoomParticipantQueryDto.java, .../mapper/RoomParticipantQueryMapper.java, .../repository/RoomQueryRepository.java, .../repository/RoomQueryRepositoryImpl.java, .../RoomQueryPersistenceAdapter.java, .../RoomGetHomeJoinedListService.java, .../RoomQueryPort.java, .../RoomGetHomeJoinedListQuery.java
홈 참여방 조회를 페이지→커서 기반으로 전환: 새로운 RoomParticipantQueryDto 및 QueryDSL 기반 repo 구현, 포트/서비스/어댑터/응답 DTO와 매퍼 변경, nextCursor/isLast 도입 및 다중 키 커서 규칙 적용.
RoomPost (Record Pin)
src/main/java/konkuk/thip/roompost/application/port/in/RecordPinUseCase.java, .../RecordPinQuery.java, .../RecordPinService.java, .../RecordPinResponse.java
RecordPinQuery에 @builder 추가, 유스케이스/서비스 반환 타입을 BookPinResult로 변경, 컨트롤러 응답 매핑 조정.
Tests
src/test/java/konkuk/thip/book/adapter/in/web/BookGetSelectableListApiTest.java, .../BookShowSavedListApiTest.java, .../room/adapter/in/web/RoomGetHomeJoinedRoomsApiTest.java, .../book/adapter/in/web/BookMostSearchedBooksApiTest.java, -(삭제) src/test/java/konkuk/thip/room/adapter/in/web/RoomGetHomeJoinedRoomsControllerTest.java
통합/단위 테스트를 커서 기반 시나리오로 재작성·확장(픽스처·정렬·커서 검증), 페이지 검증 단위 테스트 삭제.
Misc Cleanup
-(삭제) src/main/java/konkuk/thip/book/application/port/in/dto/DummyCommand.java, -(삭제) src/main/java/konkuk/thip/book/application/port/in/dto/DummyQuery.java
사용되지 않는 더미 DTO 제거.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as 사용자
  participant C as BookQueryController
  participant S as BookSavedListService
  participant P as BookQueryPort
  participant R as BookQueryRepository

  U->>C: GET /books/saved?cursor=...
  C->>S: getSavedBookList(userId, cursor)
  S->>P: findSavedBooksBySavedAt(userId, Cursor)
  P->>R: findSavedBooksBySavedAt(userId, lastSavedAt, pageSize)
  R-->>P: List<BookQueryDto>
  P-->>S: CursorBasedList(contents, nextCursor, isLast)
  S-->>C: BookShowSavedListResponse.of(contents, nextCursor, isLast)
  C-->>U: 200 OK (body)
Loading
sequenceDiagram
  autonumber
  actor U as 사용자
  participant C as RoomQueryController
  participant S as RoomGetHomeJoinedListService
  participant P as RoomQueryPort
  participant R as RoomQueryRepository

  U->>C: GET /rooms/home/joined?cursor=...
  C->>S: getHomeJoinedRoomList(userId, cursor)
  S->>P: searchHomeJoinedRooms(userId, Cursor)
  P->>R: findHomeJoinedRoomsByUserPercentage(userId, pctCursor, startDateCursor, roomIdCursor, pageSize)
  R-->>P: List<RoomParticipantQueryDto>
  P-->>S: CursorBasedList(contents, nextCursor, isLast)
  S-->>C: RoomGetHomeJoinedListResponse.of(mappedContents, nickname, nextCursor, isLast)
  C-->>U: 200 OK (body)
Loading
sequenceDiagram
  autonumber
  actor U as 사용자
  participant C as RoomPostQueryController
  participant S as RecordPinService
  participant M as BookQueryMapper

  U->>C: POST /rooms/{id}/records/{recordId}/pin
  C->>S: pinRecord(RecordPinQuery.builder(...).build())
  S->>M: toBookPinResult(book)
  M-->>S: BookPinResult
  S-->>C: BookPinResult
  C-->>U: BaseResponse.ok(RecordPinResponse.of(BookPinResult))
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Assessment against linked issues

Objective Addressed Explanation
모임 홈 진행중인 방 조회를 페이지→커서 기반으로 수정 (#283)
저장한 책 조회에 커서 기반 무한스크롤 추가 (#283)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Feed 저장 목록 메서드명 변경: findSavedFeedsByCreatedAtfindSavedFeedsBySavedAt (src/main/java/konkuk/thip/feed/...) 링크 이슈는 책·모임 홈의 커서 전환 및 저장한 책 누락 보완에 초점; 피드 네이밍 정비는 명시적 목표에 포함되지 않음.
RecordPin 반환타입 변경 및 관련 매핑 조정 (src/main/java/konkuk/thip/roompost/...) 이슈는 무한스크롤(커서) 관련 작업으로 보이며, RecordPin의 반환형 변경은 해당 이슈의 핵심 요구사항으로 명시되지 않음.
Dummy DTO 삭제 (src/main/java/konkuk/thip/book/application/port/in/dto/DummyCommand.java, .../DummyQuery.java) 더미 DTO 제거는 정리성 변경으로, 이슈의 기능적 목표와 직접 관련되지 않음.

Possibly related PRs

Suggested reviewers

  • seongjunnoh

Poem

토끼가 속삭여요, 스크롤은 커서로 이어지고 🥕
nextCursor 꺼내면 다음 줄이 반짝이고
저장한 책들 줄지어 정렬되고
참여방은 퍼센트로 춤을 추네
마지막엔 isLast가 살짝 고개를 끄덕여주네


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b835fb9 and bea79e5.

📒 Files selected for processing (4)
  • src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java (2 hunks)
  • src/main/java/konkuk/thip/book/adapter/out/persistence/repository/BookJpaRepository.java (1 hunks)
  • src/main/java/konkuk/thip/book/application/port/out/BookQueryPort.java (2 hunks)
  • src/test/java/konkuk/thip/book/adapter/in/web/BookGetSelectableListApiTest.java (2 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/#283-cursor-book

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.
    • 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.
  • 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 the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit 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

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • 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.

@github-actions
Copy link

github-actions bot commented Aug 28, 2025

Test Results

435 tests   435 ✅  38s ⏱️
128 suites    0 💤
128 files      0 ❌

Results for commit bea79e5.

♻️ This comment has been updated with latest results.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksApiTest.java (2)

65-72: 중복된 nicknameUpdatedAt 설정 제거
7개월 전(.minusMonths(7))과 1개월 전(.minusMonths(1)) 설정이 중복되어, 두 번째 호출이 첫 번째 값을 덮어씁니다. 의도한 값 하나만 남겨주세요.


58-60: app.redis.search-rank-detail-prefix 프로퍼티 정의 추가 필요
application 설정 파일(application.yml 또는 application.properties)의 기본 및 테스트 프로파일에 app.redis.search-rank-detail-prefix 값을 반드시 추가하세요.

src/main/java/konkuk/thip/feed/application/port/out/FeedQueryPort.java (1)

34-41: findSavedFeedsBySavedAt 구현부를 savedAt 기준으로 수정 필요

  • FeedQueryPersistenceAdapter의 findSavedFeedsBySavedAt 내부에서 여전히 feedJpaRepository.findSavedFeedsByCreatedAt을 호출하고, Cursor에도 savedCreatedAt을 사용 중 → 메서드명·파라미터·쿼리 모두 savedAt 컬럼 기반으로 변경
  • FeedQueryRepository 및 RepositoryImpl에서 findSavedFeedsByCreatedAtfindSavedFeedsBySavedAt으로 이름·파라미터(lastSavedAt→lastSavedAt)·QueryDSL 조건(savedFeed.savedAt) 수정
  • JavaDoc(34~36행)에 “저장일자(savedAt) 기준 커서 구성” 문구 추가
src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedQueryPersistenceAdapter.java (1)

110-119: 복합 커서(savedCreatedAt, feedId) 적용 필요

  • findSavedFeedsBySavedAt/findSavedFeedsByCreatedAt 페이징에 savedCreatedAt만 사용 시, 동일 타임스탬프 그룹의 일부가 누락됨.
  • CursorsavedCreatedAtfeedId 둘 다 담고, Repository 쿼리 ORDER BY savedCreatedAt DESC, feed.postId DESC 및 경계 조건을
    savedCreatedAt < lastSavedAt OR (savedCreatedAt = lastSavedAt AND postId < lastFeedId) 로 확장하세요.
🧹 Nitpick comments (41)
src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksApiTest.java (3)

75-78: Redis 정리 누락 — 테스트 간 데이터 누수 방지 코드 제안

Redis에 어제 키를 쓰는 테스트가 있어(라인 83~109) tearDown에서 prefix 기반 정리를 권장합니다.

다음 패치를 고려해주세요:

 void tearDown() {
-        userJpaRepository.deleteAllInBatch();
+        userJpaRepository.deleteAllInBatch();
+        // Redis 테스트 데이터 정리
+        java.util.Set<String> keys = redisTemplate.keys(searchRankDetailPrefix + "*");
+        if (keys != null && !keys.isEmpty()) {
+            redisTemplate.delete(keys);
+        }
 }

81-83: 테스트 명세와 데이터 불일치(Top 5 vs 실제 3건)

표제는 Top 5지만 3건만 검증합니다. 혼동 방지를 위해 명세를 맞추세요.

-    @DisplayName("어제 랭킹 Top 5를 정상적으로 조회한다")
+    @DisplayName("어제 랭킹 Top 3를 정상적으로 조회한다")

(대안: 테스트 데이터를 5건으로 확장)


52-55: userId 조회 방식 단순화 제안

매 테스트에서 findAll().get(0) 대신 setUp에서 저장한 사용자 ID를 필드로 보관해 쓰면 가독성과 안전성이 좋아집니다.

@@
     @Autowired
     private ObjectMapper objectMapper;
 
+    private Long userId;
@@
-        UserJpaEntity user = userJpaRepository.save(UserJpaEntity.builder()
+        UserJpaEntity user = userJpaRepository.save(UserJpaEntity.builder()
                 .oauth2Id("kakao_432708231")
                 .nickname("User1")
                 .nicknameUpdatedAt(LocalDate.now().minusMonths(7))
-                .nicknameUpdatedAt(LocalDate.now().minusMonths(1))
                 .role(UserRole.USER)
                 .alias(alias)
                 .build());
+        this.userId = user.getUserId();
@@
-        Long userId = userJpaRepository.findAll().get(0).getUserId();
+        Long userId = this.userId;
@@
-        Long userId = userJpaRepository.findAll().get(0).getUserId();
+        Long userId = this.userId;

Also applies to: 65-73, 110-111, 143-144

src/main/java/konkuk/thip/common/util/Cursor.java (1)

78-80: Double 커서 파싱 추가는 적절합니다. 비유한정값(NaN/Infinity) 가드 추가 제안

커서 비교/정렬 안정성을 위해 NaN/Infinity는 조기 예외로 막는 편이 안전합니다. 아래처럼 간단히 보강하세요.

-    public Double getDouble(int index) {
-        return getAs(index, Double::parseDouble, "Double");
-    }
+    public Double getDouble(int index) {
+        Double v = getAs(index, Double::parseDouble, "Double");
+        if (v.isNaN() || v.isInfinite()) {
+            throw new IllegalArgumentException("커서 Double 값은 NaN/Infinity를 허용하지 않습니다: '" + get(index) + "'");
+        }
+        return v;
+    }
src/main/java/konkuk/thip/book/adapter/in/web/response/BookSelectableListResponse.java (1)

6-9: 페이징 계약 명확화: nextCursor와 isLast의 중복 상태 방지

isLast=true일 때 nextCursor가 비어 있지 않으면 모호합니다. 불변식을 “isLast=true ⇒ nextCursor=null”로 고정하는 것을 권장합니다(아래 팩토리 보강안 참고).

API 응답 계약서/테스트에서 해당 불변식을 검증하는지 확인 부탁드립니다.

src/main/java/konkuk/thip/room/application/port/out/dto/RoomParticipantQueryDto.java (4)

22-22: 오타: 에러 메시지의 필드명이 불일치합니다

roomTitle 필드를 검증하면서 메시지는 roomName으로 표기되어 있습니다. 디버깅 혼선을 줄이기 위해 일치시키세요.

-        Assert.notNull(roomTitle, "roomName must not be null");
+        Assert.notNull(roomTitle, "roomTitle must not be null");

15-16: API 응답과 타입/스케일 불일치 가능성 (Double → int 매핑)

이 DTO의 userPercentageDouble인데, 응답 DTO(JoinedRoomInfo)는 int입니다. 반올림/버림/올림 규칙을 명시적으로 맞추고 매퍼에서 고정하세요(예: Math.round).


20-25: 도메인 제약 검증 보강 제안 (범위 검증 추가)

Null 검증만으로는 부족합니다. 정렬/커서 계산의 안정성을 위해 합리적 범위를 강제하세요.

     public RoomParticipantQueryDto {
         Assert.notNull(roomId, "roomId must not be null");
         Assert.notNull(bookImageUrl, "bookImageUrl must not be null");
         Assert.notNull(roomTitle, "roomTitle must not be null");
         Assert.notNull(memberCount, "memberCount must not be null");
         Assert.notNull(userPercentage, "userPercentage must not be null");
         Assert.notNull(startDate, "startDate must not be null");
+        Assert.isTrue(memberCount >= 0, "memberCount must be >= 0");
+        Assert.isTrue(userPercentage >= 0 && userPercentage <= 100,
+                "userPercentage must be between 0 and 100");
     }

9-16: 이름 규칙 일관성 및 원시 타입 사용 검토

  • 다른 DTO(RoomQueryDto)는 roomName을 사용합니다. 본 DTO/응답은 roomTitle을 사용합니다. 한쪽으로 통일을 검토하세요.
  • 널을 허용하지 않는 필드(memberCount, userPercentage)는 wrapper 대신 primitive(int, double) 사용을 고려하면 NPE 리스크를 더 줄일 수 있습니다(필요 시 기본값 명확화).
src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetHomeJoinedListResponse.java (1)

18-21: of 팩토리 메서드: 가독성 및 제네릭 축약

동일 클래스의 내부형은 FQCN 없이 사용할 수 있습니다. 서식도 정리하면 가독성이 좋아집니다.

-    public static RoomGetHomeJoinedListResponse of(List<RoomGetHomeJoinedListResponse.JoinedRoomInfo> roomList,
-                                                   String nickname, String nextCursor, boolean isLast){
-    return new RoomGetHomeJoinedListResponse(roomList, nickname, nextCursor, isLast);}
+    public static RoomGetHomeJoinedListResponse of(
+            List<JoinedRoomInfo> roomList,
+            String nickname,
+            String nextCursor,
+            boolean isLast
+    ) {
+        return new RoomGetHomeJoinedListResponse(roomList, nickname, nextCursor, isLast);
+    }
src/main/java/konkuk/thip/book/adapter/in/web/response/BookShowSavedListResponse.java (1)

10-19: isSaved 필드의 정보 가치 재검토

“저장한 책 목록” 응답이라면 isSaved는 항상 true일 가능성이 높습니다. 중복 정보면 제거하거나, 공용 DTO 재사용 의도라면 주석으로 문맥을 명시하세요.

src/main/java/konkuk/thip/book/application/port/out/dto/BookQueryDto.java (1)

5-6: @nullable 패키지 선택 재검토 (JSR-305 의존성 회피)

javax.annotation.Nullable는 환경에 따라 의존성 추가가 필요합니다. 스프링을 사용 중이니 org.springframework.lang.Nullable로 전환을 권장합니다.

-import javax.annotation.Nullable;
+import org.springframework.lang.Nullable;

Also applies to: 15-16

src/main/java/konkuk/thip/roompost/application/port/in/RecordPinUseCase.java (1)

3-3: 레이어/모듈 결합도 점검

roompost의 use case가 book 모듈의 DTO를 직접 반환합니다. 의도된 공용 DTO라면 OK입니다. 아니라면 application 계층의 공용 결과 타입으로 추상화하거나 매퍼를 어댑터 계층으로 밀어내는 방식을 고려하세요.

src/main/java/konkuk/thip/feed/application/service/FeedShowSavedListService.java (1)

39-41: 빈 결과일 때 좋아요 조회 스킵으로 쿼리 1회 절감

result.contents()가 비어있다면 postLikeQueryPort.findPostIdsLikedByUser 호출을 생략하고 즉시 응답을 반환하세요. 빈 목록에서의 불필요한 DB I/O를 줄일 수 있습니다.

적용 예시:

     CursorBasedList<FeedQueryDto> result = feedQueryPort.findSavedFeedsBySavedAt(userId, nextCursor);
-    Set<Long> feedIds = result.contents().stream()
+    if (result.contents().isEmpty()) {
+        return new FeedShowSavedListResponse(
+            java.util.List.of(),
+            result.nextCursor(),
+            !result.hasNext()
+        );
+    }
+    Set<Long> feedIds = result.contents().stream()
         .map(FeedQueryDto::feedId)
         .collect(Collectors.toUnmodifiableSet());

필요 시 상단에 import java.util.List;를 추가하세요.

src/main/java/konkuk/thip/roompost/adapter/in/web/response/RecordPinResponse.java (1)

11-17: Null 방어 추가 권장

정적 팩토리에서 book이 null일 경우 NPE가 발생합니다. 명시적 방어 코드로 실패 지점을 선명히 하세요.

+import java.util.Objects;
 ...
     static public RecordPinResponse of(BookPinResult book) {
-        return new RecordPinResponse(
+        Objects.requireNonNull(book, "book must not be null");
+        return new RecordPinResponse(
                 book.bookTitle(),
                 book.authorName(),
                 book.bookImageUrl(),
                 book.isbn()
         );
     }
src/main/java/konkuk/thip/book/adapter/out/persistence/repository/BookJpaRepository.java (1)

8-8: Spring Data 커스텀 리포지토리 결합은 적절. 네이밍/스타일 한 건 보완 권고

  • 구현체가 BookQueryRepositoryImpl로 정확히 존재(동일 패키지/네이밍)하는지 확인해 주세요. 미스매치 시 런타임 조합 실패합니다.
  • 사소하지만 컨벤션상 콤마 뒤 공백 추가 제안.

적용 diff:

-public interface BookJpaRepository extends JpaRepository<BookJpaEntity, Long>,BookQueryRepository {
+public interface BookJpaRepository extends JpaRepository<BookJpaEntity, Long>, BookQueryRepository {
src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java (1)

28-28: 홈 참여 방 커서 API: 정렬 키/커서 스펙 주석화 및 시간 의존성 검증 필요

  • 정렬 기준(유저 진행도 desc → 활동 시작일 asc → roomId asc)과 커서 해석(복합 키)을 메서드 Javadoc으로 명시해 주세요. 구현-호출 간 불일치 방지용입니다.
  • 기존 today 파라미터 제거로 시스템 시계 의존 가능성↑. 저장소/서비스에서 Clock 주입 등으로 테스트 가능성과 타임존 결정성을 확보했는지 확인 바랍니다.

예시 Javadoc(제안):

+    /**
+     * 모임 홈 - 참여중인 방 조회.
+     * 정렬: userPercentage DESC, activityStart ASC, roomId ASC.
+     * 커서: 위 복합 키 기반(Cursor 유틸로 직렬화/비교).
+     */
     CursorBasedList<RoomParticipantQueryDto> searchHomeJoinedRooms(Long userId, Cursor cursor);
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepository.java (1)

24-24: 커서 정렬 키에 대한 DB 인덱스 설계 제안

  • 예상 where/order: (user_id = ?) ORDER BY user_percentage DESC, activity_start ASC, room_id ASC LIMIT ?
  • 아래와 같은 복합 인덱스 고려: (user_id, user_percentage DESC, activity_start ASC, room_id ASC).
src/main/java/konkuk/thip/book/adapter/out/persistence/repository/BookQueryRepository.java (2)

9-11: 첫 페이지 커서(null) 처리 계약 명시 필요

  • lastSavedAt/lastRoomPercentage가 null일 수 있는지, 그 경우의 정렬/경계 조건을 Javadoc으로 명시해 주세요.

9-11: 커서 정렬 키에 대한 DB 인덱스 설계 제안

  • 저장한 책: (user_id, saved_at ASC, book_id ASC)
  • 참여중인 방의 책: (user_id, room_percentage DESC, book_id ASC) 또는 진행도/tie-breaker 조합 인덱스
src/main/java/konkuk/thip/roompost/adapter/in/web/RoomPostQueryController.java (1)

83-87: 빌더 체이닝 포맷 일관화 (가독성 개선)
현재 한 줄 끝의 점(.)과 다음 줄 시작의 메서드 호출이 혼재되어 있습니다. 모두 “줄 시작에 점(.)” 스타일로 통일하면 가독성과 리뷰 용이성이 좋아집니다.

-        return BaseResponse.ok(RecordPinResponse.of(recordPinUseCase.pinRecord(
-                RecordPinQuery.builder()
-                        .roomId(roomId).
-                        recordId(recordId)
-                        .userId(userId).build())));
+        return BaseResponse.ok(RecordPinResponse.of(
+                recordPinUseCase.pinRecord(
+                        RecordPinQuery.builder()
+                                .roomId(roomId)
+                                .recordId(recordId)
+                                .userId(userId)
+                                .build()
+                )
+        ));
src/main/java/konkuk/thip/book/adapter/in/web/BookQueryController.java (1)

95-103: RequestParam를 Enum으로 직접 바인딩하여 변환 로직 제거
String type을 받아 BookSelectableType.from(type)로 변환하는 대신, 컨트롤러에서 BookSelectableType을 직접 받으면 유효성/스웨거 명확성과 코드 단순성이 향상됩니다.

-    public BaseResponse<BookSelectableListResponse> showSelectableBookList(
-            @Parameter(description = "저장한 책 또는 참여 중인 모임의 책을 구분하는 필드 (SAVED : 저장한 책 / JOINING : 모임 방의 책)", example = "SAVED") @RequestParam final String type,
+    public BaseResponse<BookSelectableListResponse> showSelectableBookList(
+            @Parameter(description = "저장한 책 또는 참여 중인 모임의 책을 구분하는 필드 (SAVED : 저장한 책 / JOINING : 모임 방의 책)", example = "SAVED") @RequestParam final BookSelectableType type,
             @Parameter(hidden = true) @UserId final Long userId,
             @Parameter(description = "커서 (첫번째 요청시 : null, 다음 요청시 : 이전 요청에서 반환받은 nextCursor 값)")
             @RequestParam(required = false) final String cursor)
     {
-        return BaseResponse.ok(
-                bookSelectableListUseCase.getSelectableBookList(BookSelectableType.from(type), userId, cursor)
-        );
+        return BaseResponse.ok(
+                bookSelectableListUseCase.getSelectableBookList(type, userId, cursor)
+        );
     }
src/main/java/konkuk/thip/book/application/service/BookSavedListService.java (1)

18-18: 페이지 크기 상수의 중복 제거 및 중앙화 제안
DEFAULT_PAGE_SIZE = 10이 여러 서비스에 중복됩니다. 공용 상수(예: PaginationDefaults.BOOK_PAGE_SIZE)로 중앙화하면 운영 중 파라미터 튜닝이 용이합니다.

+import konkuk.thip.common.util.PaginationDefaults;
@@
-    private static final int DEFAULT_PAGE_SIZE = 10;
+    private static final int DEFAULT_PAGE_SIZE = PaginationDefaults.BOOK_PAGE_SIZE;

추가(새 파일 예시):

package konkuk.thip.common.util;

public final class PaginationDefaults {
    private PaginationDefaults() {}
    public static final int BOOK_PAGE_SIZE = 10;
}
src/main/java/konkuk/thip/book/application/port/out/BookQueryPort.java (1)

13-16: 정렬/커서 불변 조건을 Javadoc으로 명시
호출자/구현체 간 계약을 명확히 하려면 정렬 키, 동순/역순, 커서 구성(단일/복합)과 “strict total ordering” 보장을 주석으로 박아두는 것을 권장합니다.

 public interface BookQueryPort {
@@
-    CursorBasedList<BookQueryDto> findSavedBooksBySavedAt(Long userId, Cursor cursor);
+    /**
+     * SavedAt 오름차순 단일 커서 기반으로 조회합니다.
+     * - Order: saved_at ASC, (tie-breaker: implementation-defined, e.g., book_id ASC)
+     * - Cursor: saved_at(, book_id)
+     */
+    CursorBasedList<BookQueryDto> findSavedBooksBySavedAt(Long userId, Cursor cursor);
@@
-    CursorBasedList<BookQueryDto> findJoiningRoomsBooksByRoomPercentage(Long userId, Cursor cursor);
+    /**
+     * 참여중인 방의 책을 복합 커서로 조회합니다.
+     * - Order: room_percentage DESC, book_id ASC
+     * - Cursor: room_percentage, book_id
+     */
+    CursorBasedList<BookQueryDto> findJoiningRoomsBooksByRoomPercentage(Long userId, Cursor cursor);
src/main/java/konkuk/thip/book/application/service/BookSelectableListService.java (1)

19-19: 페이지 크기 상수 중앙화(중복 제거)
동일한 상수 중복을 방지하기 위해 PaginationDefaults 사용을 제안합니다. (BookSavedListService와 동일 제안)

+import konkuk.thip.common.util.PaginationDefaults;
@@
-    private static final int DEFAULT_PAGE_SIZE = 10;
+    private static final int DEFAULT_PAGE_SIZE = PaginationDefaults.BOOK_PAGE_SIZE;
src/main/java/konkuk/thip/room/application/mapper/RoomParticipantQueryMapper.java (1)

12-16: MapStruct imports에 불필요한 DateUtil 제거

이 매퍼에서 DateUtil을 사용하지 않습니다. 불필요한 import 설정을 제거해 코드 가독성과 유지보수성을 높여주세요.

 @Mapper(
-        componentModel = "spring",
-        imports = DateUtil.class,
-        unmappedTargetPolicy = ReportingPolicy.IGNORE       // 명시적으로 매핑하지 않은 필드를 무시하도록 설정
+        componentModel = "spring",
+        unmappedTargetPolicy = ReportingPolicy.IGNORE       // 명시적으로 매핑하지 않은 필드를 무시하도록 설정
 )
src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedQueryPersistenceAdapter.java (2)

114-114: 명칭 불일치: 저장소 메서드명 정합성

어댑터는 findSavedFeedsBySavedAt로 개명되었지만 저장소 호출은 findSavedFeedsByCreatedAt입니다. 의미적 혼동을 줄이기 위해 저장소 메서드도 SavedAt으로 통일하세요.

-        List<FeedQueryDto> feedQueryDtos = feedJpaRepository.findSavedFeedsByCreatedAt(userId, lastSavedAt, size);
+        List<FeedQueryDto> feedQueryDtos = feedJpaRepository.findSavedFeedsBySavedAt(userId, lastSavedAt, size);

116-118: Null-safe 직렬화

savedCreatedAt가 null일 가능성이 있다면 toString()은 NPE를 유발합니다. String.valueOf로 방어하세요.

-            Cursor nextCursor = new Cursor(List.of(feedQueryDto.savedCreatedAt().toString()));
+            Cursor nextCursor = new Cursor(List.of(String.valueOf(feedQueryDto.savedCreatedAt())));
src/main/java/konkuk/thip/room/application/service/RoomGetHomeJoinedListService.java (1)

20-21: 페이지 사이즈 하드코딩 — 설정화 권장

DEFAULT_PAGE_SIZE(10) 하드코딩 대신 환경설정 주입(@value 또는 ConfigProperties)으로 전환해 운영 중 유연하게 조정할 수 있게 하세요.

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

88-94: Swagger 문서에 커서 예시 추가 제안

클라이언트 연동 편의를 위해 cursor 파라미터 예시(Base64 등 인코딩된 값)를 제공하면 좋습니다.

-            @Parameter(description = "커서 (첫번째 요청시 : null, 다음 요청시 : 이전 요청에서 반환받은 nextCursor 값)")
+            @Parameter(description = "커서 (첫번째 요청시 : null, 다음 요청시 : 이전 요청에서 반환받은 nextCursor 값)", example = "WyIyMD25-08-21T10:35:12\", \"12345\"]")
src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java (1)

72-90: 복합 정렬 및 커서 안전성 검토

  • 정렬 순서: QueryDSL 구현부에서 participant.userPercentage.desc(), room.startDate.asc(), room.roomId.asc()로 정확히 일치합니다.
  • null 안전성: dto.userPercentage()가 null일 경우 NPE가 발생할 수 있으니, 필요 시 String.valueOf(dto.userPercentage())로 변경 검토하세요.
  • 부동소수점 정밀도: Double 사용 시 경계부에서 오차가 발생할 수 있으므로, 정밀도가 중요하다면 BigDecimal 또는 정수(%) 사용을 고려하세요.
src/test/java/konkuk/thip/book/adapter/in/web/BookShowSavedListApiTest.java (2)

74-82: 직접 SQL로 created_at 덮어쓰는 테스트는 깨지기 쉬움

DB 테이블/컬럼명 변경 시 테스트가 바로 깨집니다. JPA 엔티티에 테스트 전용 setter(패키지 접근) 혹은 테스트용 업데이트 리포지토리 메서드를 도입해 의존도를 낮이는 것을 권장합니다.

가능한 방향:

- jdbcTemplate.update("UPDATE saved_books SET created_at = ? WHERE saved_id = ?", ...);
+ savedBookTestRepository.updateCreatedAt(sb1.getSavedId(), baseTime.minusMinutes(1));

141-161: 페이지 크기 명시로 테스트 의도 고정화

현재 기본 size(10)에 의존합니다. 향후 기본값이 바뀌어도 테스트가 안정적으로 동작하도록 size 파라미터를 명시하는 것을 추천합니다.

- ResultActions firstPage = mockMvc.perform(get("/books/saved")
-         .requestAttr("userId", me.getUserId())
- );
+ ResultActions firstPage = mockMvc.perform(get("/books/saved")
+         .requestAttr("userId", me.getUserId())
+         .param("size", "10")
+ );

Also applies to: 169-181

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

255-315: 첫 페이지 요청에도 size 고정 권장

두 번째 페이지만 size=10을 전달합니다. 첫 페이지도 동일하게 지정하면 테스트 의도가 더 명확해집니다.

- ResultActions firstPage = mockMvc.perform(get("/rooms/home/joined")
-         .requestAttr("userId", user.getUserId()));
+ ResultActions firstPage = mockMvc.perform(get("/rooms/home/joined")
+         .requestAttr("userId", user.getUserId())
+         .param("size", "10"));

116-128: 테스트 헬퍼 역할 명시성 개선 제안

saveSingleUserToRoom가 항상 HOST로 저장합니다. 데이터 시나리오에 따라 MEMBER가 필요한 케이스가 있을 수 있으니 role을 인자로 받도록 확장하는 것이 재사용성에 유리합니다.

-private void saveSingleUserToRoom(RoomJpaEntity roomJpaEntity, UserJpaEntity userJpaEntity, Double userPercentage) {
+private void saveSingleUserToRoom(RoomJpaEntity roomJpaEntity, UserJpaEntity userJpaEntity, Double userPercentage, RoomParticipantRole role) {
-    RoomParticipantJpaEntity roomParticipantJpaEntity = RoomParticipantJpaEntity.builder()
+    RoomParticipantJpaEntity roomParticipantJpaEntity = RoomParticipantJpaEntity.builder()
         .userJpaEntity(userJpaEntity)
         .roomJpaEntity(roomJpaEntity)
-        .roomParticipantRole(RoomParticipantRole.HOST)
+        .roomParticipantRole(role)
         .userPercentage(userPercentage)
         .build();
src/test/java/konkuk/thip/book/adapter/in/web/BookGetSelectableListApiTest.java (1)

168-244: 테스트 클래스 책임 분리 제안 (중복 테스트 최소화)

본 클래스에서 /books/saved 엔드포인트를 검증하고 있는데, 같은 성격의 테스트가 BookShowSavedListApiTest에도 존재합니다. 엔드포인트별로 테스트 클래스를 분리하면 중복과 유지보수 비용을 줄일 수 있습니다.

src/main/java/konkuk/thip/book/adapter/out/persistence/repository/BookQueryRepositoryImpl.java (2)

72-73: 집계 표현 간소화

Expressions.numberTemplate 대신 room.roomPercentage.max() 사용이 더 간결하고 타입 안전합니다.

- NumberExpression<Double> maxRoomPercentage = Expressions.numberTemplate(Double.class, "max({0})", room.roomPercentage);
+ NumberExpression<Double> maxRoomPercentage = room.roomPercentage.max();

111-112: 주석 오타

"방 아이디" → "책 아이디"가 맞습니다.

- .orderBy(maxRoomPercentage.desc(), book.bookId.asc()) // 방 진행도 높은 순 (내림차순), 같으면 방 아이디 작은 순 (오름차순)
+ .orderBy(maxRoomPercentage.desc(), book.bookId.asc()) // 방 진행도 높은 순 (내림차순), 같으면 책 아이디 작은 순 (오름차순)
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java (2)

201-217: LocalDate.now() 다중 호출 제거로 경계 시점 불일치 예방

메서드 내 today를 한 번만 캡처해 사용하세요. 미세하지만 경계 시점 불일치(자정 경계 등)를 예방합니다.

적용 diff:

 public List<RoomParticipantQueryDto> findHomeJoinedRoomsByUserPercentage(
 ...
     int pageSize) {

-        // 검색 조건(where) 조립
+        // 검색 조건(where) 조립
+        final LocalDate today = LocalDate.now();
         // 유저가 참여한 방만: userId 조건
         // 활동 기간 중인 방만: startDate ≤ today ≤ endDate
         BooleanBuilder where = new BooleanBuilder();
         where.and(participant.userJpaEntity.userId.eq(userId));
         where.and(participant.status.eq(ACTIVE));
         where.and(room.status.eq(ACTIVE));
-        where.and(room.startDate.loe(LocalDate.now()));
-        where.and(room.endDate.goe(LocalDate.now()));
+        where.and(room.startDate.loe(today));
+        where.and(room.endDate.goe(today));

381-406: CursorDate 용도 명확화 권장
QRoomQueryDto 7-인자 생성자에서 record의 endDate 필드에 room.startDate(cursorExpr)를 매핑해 dto.endDate()를 커서로 사용하고 있는데, 기능상 의도된 동작이지만 ‘endDate’가 실제 이벤트 종료일과 커서 용도로 혼용되어 유지보수 혼란이 발생할 수 있습니다. cursorDate로 필드명을 변경하거나, 검색 전용 DTO/생성자로 분리하는 리팩토링을 고려하세요.

src/main/java/konkuk/thip/book/application/mapper/BookQueryMapper.java (1)

38-42: isSaved 상수 매핑 타입 명확화 제안

isSaved가 Boolean일 경우 명시적 타이핑이 더 안전합니다. (primitive boolean이면 현 상태 유지 OK)

  • 확인: BookShowSavedListResponse.BookShowSavedDto.isSaved 타입(Boolean vs boolean)

선택적 변경 diff(Boolean일 때만):

-    @Mapping(target = "isSaved", constant = "true")
+    @Mapping(target = "isSaved", expression = "java(Boolean.TRUE)")
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3153d83 and 944daa8.

📒 Files selected for processing (41)
  • src/main/java/konkuk/thip/book/adapter/in/web/BookQueryController.java (2 hunks)
  • src/main/java/konkuk/thip/book/adapter/in/web/response/BookSelectableListResponse.java (1 hunks)
  • src/main/java/konkuk/thip/book/adapter/in/web/response/BookShowSavedListResponse.java (1 hunks)
  • src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java (2 hunks)
  • src/main/java/konkuk/thip/book/adapter/out/persistence/repository/BookJpaRepository.java (1 hunks)
  • src/main/java/konkuk/thip/book/adapter/out/persistence/repository/BookQueryRepository.java (1 hunks)
  • src/main/java/konkuk/thip/book/adapter/out/persistence/repository/BookQueryRepositoryImpl.java (1 hunks)
  • src/main/java/konkuk/thip/book/application/mapper/BookQueryMapper.java (2 hunks)
  • src/main/java/konkuk/thip/book/application/port/in/BookSelectableListUseCase.java (1 hunks)
  • src/main/java/konkuk/thip/book/application/port/in/BookShowSavedListUseCase.java (1 hunks)
  • src/main/java/konkuk/thip/book/application/port/in/dto/BookPinResult.java (1 hunks)
  • src/main/java/konkuk/thip/book/application/port/in/dto/DummyCommand.java (0 hunks)
  • src/main/java/konkuk/thip/book/application/port/in/dto/DummyQuery.java (0 hunks)
  • src/main/java/konkuk/thip/book/application/port/out/BookQueryPort.java (1 hunks)
  • src/main/java/konkuk/thip/book/application/port/out/dto/BookQueryDto.java (1 hunks)
  • src/main/java/konkuk/thip/book/application/service/BookSavedListService.java (1 hunks)
  • src/main/java/konkuk/thip/book/application/service/BookSelectableListService.java (1 hunks)
  • src/main/java/konkuk/thip/common/util/Cursor.java (1 hunks)
  • src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedQueryPersistenceAdapter.java (1 hunks)
  • src/main/java/konkuk/thip/feed/application/port/out/FeedQueryPort.java (1 hunks)
  • src/main/java/konkuk/thip/feed/application/service/FeedShowSavedListService.java (1 hunks)
  • src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java (1 hunks)
  • src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetHomeJoinedListResponse.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/RoomParticipantQueryMapper.java (1 hunks)
  • src/main/java/konkuk/thip/room/application/port/in/dto/RoomGetHomeJoinedListQuery.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/RoomParticipantQueryDto.java (1 hunks)
  • src/main/java/konkuk/thip/room/application/service/RoomGetHomeJoinedListService.java (2 hunks)
  • src/main/java/konkuk/thip/roompost/adapter/in/web/RoomPostQueryController.java (1 hunks)
  • src/main/java/konkuk/thip/roompost/adapter/in/web/response/RecordPinResponse.java (1 hunks)
  • src/main/java/konkuk/thip/roompost/application/port/in/RecordPinUseCase.java (1 hunks)
  • src/main/java/konkuk/thip/roompost/application/port/in/dto/record/RecordPinQuery.java (1 hunks)
  • src/main/java/konkuk/thip/roompost/application/service/RecordPinService.java (3 hunks)
  • src/test/java/konkuk/thip/book/adapter/in/web/BookGetSelectableListApiTest.java (2 hunks)
  • src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksApiTest.java (1 hunks)
  • src/test/java/konkuk/thip/book/adapter/in/web/BookShowSavedListApiTest.java (2 hunks)
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomGetHomeJoinedRoomsApiTest.java (6 hunks)
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomGetHomeJoinedRoomsControllerTest.java (0 hunks)
💤 Files with no reviewable changes (3)
  • src/main/java/konkuk/thip/book/application/port/in/dto/DummyQuery.java
  • src/main/java/konkuk/thip/book/application/port/in/dto/DummyCommand.java
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomGetHomeJoinedRoomsControllerTest.java
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-03T03:05:05.031Z
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로부터 조회하는 메서드를 추가함.

Applied to files:

  • src/main/java/konkuk/thip/book/application/port/out/BookQueryPort.java
🧬 Code graph analysis (9)
src/main/java/konkuk/thip/book/application/service/BookSavedListService.java (1)
src/main/java/konkuk/thip/book/application/service/BookSelectableListService.java (1)
  • Service (15-40)
src/test/java/konkuk/thip/book/adapter/in/web/BookShowSavedListApiTest.java (2)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
  • TestEntityFactory (30-394)
src/test/java/konkuk/thip/book/adapter/in/web/BookGetSelectableListApiTest.java (1)
  • SpringBootTest (39-397)
src/main/java/konkuk/thip/room/application/port/out/dto/RoomParticipantQueryDto.java (1)
src/main/java/konkuk/thip/room/application/port/out/dto/RoomQueryDto.java (1)
  • Builder (10-57)
src/test/java/konkuk/thip/room/adapter/in/web/RoomGetHomeJoinedRoomsApiTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
  • TestEntityFactory (30-394)
src/main/java/konkuk/thip/book/application/service/BookSelectableListService.java (1)
src/main/java/konkuk/thip/book/application/service/BookSavedListService.java (1)
  • Service (14-35)
src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java (1)
src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java (1)
  • Repository (21-89)
src/main/java/konkuk/thip/room/application/mapper/RoomParticipantQueryMapper.java (2)
src/main/java/konkuk/thip/common/util/DateUtil.java (1)
  • DateUtil (12-85)
src/main/java/konkuk/thip/room/application/mapper/RoomQueryMapper.java (1)
  • Mapper (16-43)
src/test/java/konkuk/thip/book/adapter/in/web/BookGetSelectableListApiTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
  • TestEntityFactory (30-394)
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java (1)
src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java (1)
  • Repository (21-168)
⏰ 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 (32)
src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksApiTest.java (1)

75-78: deleteAllInBatch 전환 적절 — 테스트 종료 속도 개선

배치 삭제로 정리 시간 단축 기대됩니다. 현재 클래스 범위에서는 부작용 없어 보입니다.

src/main/java/konkuk/thip/book/application/port/in/dto/BookPinResult.java (1)

3-9: DTO 축소 및 계약 검증 완료
BookPinResult 사용처(RecordPinResponse/Service/UseCase/Mapper) 전반에서 bookId·publisher 의존 흔적이 없음을 확인했습니다.

src/main/java/konkuk/thip/roompost/application/port/in/dto/record/RecordPinQuery.java (1)

3-6: annotationProcessor 설정 확인됨 — Lombok 버전(≥1.18.20) 확인 필요
build.gradle에 이미 annotationProcessor 'org.projectlombok:lombok' 설정이 존재합니다. 레코드 @Builder 지원을 위해 사용 중인 Lombok 버전이 최소 1.18.20 이상인지 확인하세요.

src/main/java/konkuk/thip/book/adapter/in/web/response/BookSelectableListResponse.java (1)

10-17: 중첩 DTO 필드 널 처리 확인

publisher, bookImageUrl은 외부 데이터 품질에 따라 빈값/널 가능성이 있습니다. 직렬화 시 널 허용/치환 정책(예: 빈 문자열 치환)을 컨트롤러/매퍼 단계에서 통일해 주세요.

테스트에 널/빈값 케이스 포함 권장.

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

11-17: 퍼센트 스케일 고정 확인 (int 0–100 보장 여부)

userPercentage가 int로 노출됩니다. 매퍼에서 0–100 범위 및 반올림 정책을 강제했는지 확인해 주세요(예: 음수/101% 차단).

src/main/java/konkuk/thip/book/adapter/in/web/response/BookShowSavedListResponse.java (1)

20-22: 팩토리 메서드 시그니처 LGTM

간결하고 명확합니다. 다른 응답 DTO들과 파라미터 순서/명명 일관성도 유지되어 있는지 최종 확인만 부탁드립니다.

src/main/java/konkuk/thip/book/application/port/out/dto/BookQueryDto.java (1)

33-45: 두 목적별 @QueryProjection 오버로드 구성 좋습니다

저장목록/모임문맥을 분리한 오버로드가 명확합니다. 매퍼에서 두 컨텍스트 혼입이 없는지 정도만 점검하면 됩니다.

src/main/java/konkuk/thip/roompost/application/port/in/RecordPinUseCase.java (1)

7-7: 반환 타입 변경 반영 완료
RecordPinService#pinRecord와 컨트롤러의 RecordPinResponse.ofBookPinResult 반환으로 올바르게 업데이트되었으며, 테스트 코드에 관련 호출이 없어 추가 수정 불필요합니다.

src/main/java/konkuk/thip/book/application/port/in/BookShowSavedListUseCase.java (2)

3-3: 응답 래퍼 도입 👍

BookShowSavedListResponse로 래핑해 커서/마지막 페이지 여부를 표준화한 점 적절합니다.


6-6: UseCase 시그니처를 Cursor VO로 통일
도메인 경계(UseCase)에서 String 대신 Cursor를 받아 타입 안정성과 검증 책임을 분리하세요.

+ import konkuk.thip.common.util.Cursor;
 public interface BookShowSavedListUseCase {
-    BookShowSavedListResponse getSavedBookList(Long userId, String cursor);
+    BookShowSavedListResponse getSavedBookList(Long userId, Cursor cursor);
 }

다른 UseCase 인터페이스들도 Cursor VO 사용 여부를 확인해 일관성을 유지하세요.

src/main/java/konkuk/thip/feed/application/port/out/FeedQueryPort.java (1)

40-40: 메서드명 변경 타당 — 저장 시점 기준이 명확해졌습니다

findSavedFeedsBySavedAt로 의미가 분명해졌습니다. 상위 서비스/컨트롤러와 명칭을 일치시킨 점도 👍

src/main/java/konkuk/thip/feed/application/service/FeedShowSavedListService.java (1)

33-35: 포트 호출 rename 반영 OK

저장한 피드를 저장일 기준으로 조회하도록 포트 호출을 findSavedFeedsBySavedAt로 교체한 부분 문제 없습니다. 주석의 의도도 코드와 일치합니다.

src/main/java/konkuk/thip/roompost/adapter/in/web/response/RecordPinResponse.java (1)

3-3: 타입 치환 일관성 👍

BookSelectableResultBookPinResult로의 전환이 컨트롤러/서비스와 일치합니다.

src/main/java/konkuk/thip/roompost/application/service/RecordPinService.java (2)

28-41: 반환 타입 전환 및 매퍼 사용 적절

BookPinResult 반환과 bookQueryMapper.toBookPinResult(book) 사용이 포트/컨트롤러와 일관됩니다. 읽기 전용 트랜잭션 구성도 적절합니다.


33-37: 추가 검증 불필요: record.validatePin(userId, roomId)가 이미 validateRoomId(roomId)를 호출해 roomId 일치 검증을 수행하므로 보강 없이 충분합니다.

src/main/java/konkuk/thip/book/application/port/in/BookSelectableListUseCase.java (1)

3-8: 레이어링 위반: UseCase가 web 응답 DTO를 반환 중

  • application.port.inadapter.in.web.response에 의존하면 계층 반전됩니다. 애플리케이션 레이어용 결과 DTO를 두고, 웹 어댑터에서 매핑하세요.
  • 또한, 다른 포트들처럼 String이 아닌 Cursor를 직접 받도록 통일하면 디코딩 책임이 명확해집니다.

제안 diff(애플리케이션 DTO로 교체 및 Cursor 사용):

-package konkuk.thip.book.application.port.in;
+package konkuk.thip.book.application.port.in;

-import konkuk.thip.book.adapter.in.web.response.BookSelectableListResponse;
+import konkuk.thip.common.util.Cursor;
+import konkuk.thip.book.application.port.in.dto.BookSelectableListResult;
 import konkuk.thip.book.application.port.in.dto.BookSelectableType;

 public interface BookSelectableListUseCase {
-    BookSelectableListResponse getSelectableBookList(BookSelectableType bookSelectableType, Long userId, String cursor);
+    BookSelectableListResult getSelectableBookList(BookSelectableType bookSelectableType, Long userId, Cursor cursor);
 }

비고: BookSelectableListResult는 애플리케이션 레이어에 신규 생성 필요(아이템 리스트, nextCursor, isLast 포함). 웹 응답 DTO는 컨트롤러에서 매핑.

⛔ Skipped due to learnings
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#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/book/adapter/in/web/BookQueryController.java (1)

111-116: LGTM: 저장한 책 목록 커서 파라미터 전달
컨트롤러에서 커서를 그대로 유스케이스로 위임하는 흐름이 명확합니다.

src/main/java/konkuk/thip/book/application/port/out/BookQueryPort.java (1)

13-16: find 접두사는 다른 QueryPort에서도 광범위하게 사용되는 패턴이므로 변경 불필요*
많은 QueryPort 인터페이스(PostLikeQueryPort, CommentQueryPort 등)에서 find* 접두사를 사용하고 있어, findSavedBooksBySavedAt/findJoiningRoomsBooksByRoomPercentage도 일관성에 부합합니다.

Likely an incorrect or invalid review comment.

src/main/java/konkuk/thip/book/application/service/BookSelectableListService.java (1)

26-37: LGTM: 커서 생성 및 포트 위임, 메타데이터 전달 흐름 적절

  • Cursor.from로 기본 페이지 크기 적용
  • 유형별 분기에서 포트 메서드 정확히 호출
  • nextCursor, isLast를 응답에 그대로 전달
    전체 플로우 문제없습니다.
src/main/java/konkuk/thip/room/application/service/RoomGetHomeJoinedListService.java (1)

30-32: 닉네임 조회 실패 시 예외/널 처리 확인 필요

userCommandPort.findById(...)가 존재하지 않는 사용자일 때의 동작(예외/Optional 등)을 확인해 방어 코드를 추가하세요. API 안정성을 위해 의미 있는 에러코드 매핑도 검토 바랍니다.

src/test/java/konkuk/thip/book/adapter/in/web/BookShowSavedListApiTest.java (1)

59-106: 정렬·응답 구조 검증 충분 — LGTM

최신 저장순, isLast/nextCursor, 필드 검증까지 깔끔합니다.

src/test/java/konkuk/thip/room/adapter/in/web/RoomGetHomeJoinedRoomsApiTest.java (1)

130-152: 커서 기반 응답 검증 전환 — LGTM

page 기반 기대치 제거하고 nextCursor/isLast 검증으로 전환된 부분 좋습니다.

src/test/java/konkuk/thip/book/adapter/in/web/BookGetSelectableListApiTest.java (4)

120-166: SAVED 타입 정렬·스키마 검증 — LGTM
최신 저장순, 필드 검증, isLast/nextCursor 확인 모두 적절합니다.


246-291: JOINING 정렬 로직 검증 — LGTM

방 진행도 내림차순, 동률 시 책ID 오름차순 로직을 잘 커버합니다.


293-332: 동일 도서 다중 방 시 MAX 진행도 우선 로직 — LGTM

집계(MAX) 기준 정렬 의도가 테스트로 명확히 보장됩니다.


334-396: 커서 페이지네이션 플로우 — LGTM

첫 페이지 10건 + nextCursor, 두 번째 페이지 잔여 2건 + isLast=true 흐름 적합합니다.

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

28-44: StatusType ACTIVE 정리: 사용 일관화 양호

static import로 통일되어 가독성과 일관성 좋아졌습니다.


245-249: 정렬 키가 커서 키와 정확히 일치(LGTM)

userPercentage DESC → startDate ASC → roomId ASC 정렬이 커서 필터와 일치합니다.


260-263: 참여자/방 상태 ACTIVE 조건 일괄 적용 LGTM

상태 필터 일관 적용으로 결과 정합성 좋아졌습니다.

Also applies to: 279-282, 330-335, 385-385, 415-415


232-239: RoomParticipantQueryDto 생성자 파라미터 순서 확인 필요
소스 검색 결과 해당 DTO 정의(레코드/생성자)를 찾지 못했습니다. QRoomParticipantQueryDto에 전달하는 필드 순서가 실제 RoomParticipantQueryDto 생성자 순서와 정확히 일치하는지 직접 검증해 주세요. 순서가 어긋나면 런타임 매핑 오류 또는 데이터 뒤바뀜 문제가 발생할 수 있습니다.

src/main/java/konkuk/thip/book/application/mapper/BookQueryMapper.java (2)

32-37: Book → BookPinResult 매핑 LGTM

필드 매핑이 명확하고 MapStruct 규약에 부합합니다.


43-46: BookQueryDto→BookSelectableDto 매핑은 적절하나, 필드명이 1:1 매칭되는지 스키마를 수동으로 확인해주세요.

Comment on lines +57 to 64
return CursorBasedList.of(dtos, pageSize, dto -> {
Cursor nextCursor = new Cursor(List.of(
dto.roomPercentage().toString(), // 내림차순 필드, 정렬순서 1
dto.bookId().toString() // 고유 ID, 중복 방지용
));
return nextCursor.toEncodedString();
});
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Double 기반 커서 직렬화 안정성

JOINING의 roomPercentage를 Double 문자열로 커서에 넣고 eq 비교합니다. 부동소수 오차로 eq가 실패할 수 있습니다. 가능하면 BigDecimal 스케일 고정(예: 소수 1자리)로 직렬화/파싱하거나, DB 컬럼 스케일에 맞춘 문자열 포맷을 사용하세요.

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

♻️ Duplicate comments (1)
src/main/java/konkuk/thip/book/adapter/out/persistence/repository/BookQueryRepositoryImpl.java (1)

35-36: [P1] SAVED 목록 커서 타이브레이커 누락 → 동일 createdAt에서 중복/유실 가능 (bookId 2차 정렬·커서 추가 필요)

createdAt 단일 키만으로는 안정 정렬이 보장되지 않습니다. bookId를 2차 키로 추가하고 where/orderBy를 복합 커서 규칙으로 맞춰주세요. 인터페이스/어댑터 시그니처 전파 필요.

-    public List<BookQueryDto> findSavedBooksBySavedAt(Long userId, LocalDateTime savedAtCursor, int pageSize) {
+    public List<BookQueryDto> findSavedBooksBySavedAt(Long userId, LocalDateTime savedAtCursor, Long bookIdCursor, int pageSize) {

@@
-        if (savedAtCursor != null) {
-            where.and(savedBook.createdAt.lt(savedAtCursor)); // 커서 기준: 저장일 기준 최신순
-        }
+        if (savedAtCursor != null && bookIdCursor != null) {
+            // createdAt DESC, tie → bookId DESC
+            where.and(
+                savedBook.createdAt.lt(savedAtCursor)
+                    .or(savedBook.createdAt.eq(savedAtCursor)
+                        .and(book.bookId.lt(bookIdCursor)))
+            );
+        } else if (savedAtCursor != null) {
+            where.and(savedBook.createdAt.lt(savedAtCursor));
+        }

@@
-                .orderBy(savedBook.createdAt.desc()) // 저장한 시간 최신순 (내림차순)
+                .orderBy(savedBook.createdAt.desc(), book.bookId.desc()) // 타이브레이커 추가

Also applies to: 44-46, 61-63

🧹 Nitpick comments (7)
src/main/java/konkuk/thip/book/adapter/out/persistence/repository/BookQueryRepositoryImpl.java (7)

73-74: 집계식 numberTemplate 대신 타입 세이프한 max() 사용 권장

QueryDSL 내장 집계자를 쓰면 가독성과 타입 안정성이 좋아집니다. 엔티티 필드 타입(Double/BigDecimal)에 맞춰 제네릭 형식을 확인하세요.

-        NumberExpression<Double> maxRoomPercentage = Expressions.numberTemplate(Double.class, "max({0})", room.roomPercentage);
+        NumberExpression<Double> maxRoomPercentage = room.roomPercentage.max();

88-93: 실수(eq) 기반 커서 비교는 정밀도로 인한 페이지 유실/중복 위험

roomPercentage가 Double이면 eq 비교가 불안정할 수 있습니다. DECIMAL(고정 소수)로 캐스팅하거나 ROUND 고정 소수점(예: 4자리)으로 집계·커서 값을 정규화하는 방식을 검토해주세요. 서비스/DTO 타입 영향 확인 필요.


110-111: GROUP BY 안정성: 선택 컬럼 전체를 그룹화하여 SQL 모드/DB 차이 이슈 회피

일부 DB/SQL 모드에서 PK만 groupBy 시 에러가 납니다. 선택한 book 컬럼을 모두 groupBy에 포함하는 것을 권장합니다.

-                    .groupBy(book.bookId)
+                    .groupBy(
+                        book.bookId,
+                        book.title,
+                        book.authorName,
+                        book.publisher,
+                        book.imageUrl,
+                        book.isbn
+                    )

112-112: 주석 불일치: 정렬 타이브레이커는 roomId가 아니라 bookId ASC

오해 방지를 위해 주석을 bookId 기준으로 정정해주세요.

-                    .orderBy(maxRoomPercentage.desc(), book.bookId.asc()) // 방 진행도 높은 순 (내림차순), 같으면 방 아이디 작은 순 (오름차순)
+                    .orderBy(maxRoomPercentage.desc(), book.bookId.asc()) // 방 진행도 높은 순, 같으면 책 아이디 작은 순

84-85: now() 중복 호출 최소화

동일 스냅샷 기준 보장을 위해 today 변수를 한 번만 계산해 재사용하세요.

-        where.and(room.startDate.loe(LocalDate.now()));
-        where.and(room.endDate.goe(LocalDate.now()));
+        where.and(room.startDate.loe(today));
+        where.and(room.endDate.goe(today));

추가: 메서드 상단에 변수 선언

final LocalDate today = LocalDate.now();

35-65: 테스트 커버리지: 동일 createdAt 타이 케이스 및 커서 경계 테스트 추가 요청

  • createdAt 동일 + bookId 역순 3건 → 페이지1/페이지2 경계 중복/유실 없는지.
  • size+1 hasNext 플래그 로직 검증.

원하시면 API/리포지토리 통합 테스트 스켈레톤 드립니다.


35-65: 실행계획 최적화 제안: 인덱스 권장

SAVED 쿼리용 복합 인덱스: saved_book(user_id, created_at DESC, book_id DESC). 데이터량 커질수록 효과 큼.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 787b988 and 3b4f81d.

📒 Files selected for processing (1)
  • src/main/java/konkuk/thip/book/adapter/out/persistence/repository/BookQueryRepositoryImpl.java (1 hunks)
⏰ 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 (1)
src/main/java/konkuk/thip/book/adapter/out/persistence/repository/BookQueryRepositoryImpl.java (1)

59-60: user 조인 추가로 user.status 필터 안전해짐

의존 경로 정합성 OK. 런타임 조인 누락 이슈 해결되었습니다.

buzz0331
buzz0331 previously approved these changes Aug 29, 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 +10 to 21
public record BookSelectableDto(
Long bookId,
String bookTitle,
String authorName,
String publisher,
String bookImageUrl,
String isbn
) {}

public static BookSelectableListResponse of(List<BookSelectableDto> bookList, String nextCursor, boolean isLast) {
return new BookSelectableListResponse(bookList, nextCursor, isLast);
}
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 +65
@Override
public List<BookQueryDto> findSavedBooksBySavedAt(Long userId, LocalDateTime savedAtCursor, int pageSize) {

// 검색 조건(where) 조립
// 유저가 저장한 책만: userId 조건
// 존재하는 유저만: ACTIVE
BooleanBuilder where = new BooleanBuilder();
where.and(savedBook.userJpaEntity.userId.eq(userId));
where.and(user.status.eq(ACTIVE));

if (savedAtCursor != null) {
where.and(savedBook.createdAt.lt(savedAtCursor)); // 커서 기준: 저장일 기준 최신순
}

return jpaQueryFactory
.select(new QBookQueryDto(
book.bookId,
book.title,
book.authorName,
book.publisher,
book.imageUrl,
book.isbn,
savedBook.createdAt
))
.from(savedBook)
.join(savedBook.userJpaEntity, user)
.join(savedBook.bookJpaEntity, book)
.where(where)
.orderBy(savedBook.createdAt.desc()) // 저장한 시간 최신순 (내림차순)
.limit(pageSize + 1)
.fetch();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

p3: 이거 저번에 회의에서 createdAt을 필드로 잡으면 인덱스를 걸지 않아서 조회시에 full scan을 해야 해서 성능에 안좋다고 해서 pk로 다 바꾸기로 했던 것 같은데 이 쿼리 먼저 도입해보는거 어떨까요??

한번에 다 바꾸는게 나을까요?? @seongjunnoh @hd0rable 두 분 의견에 따르겠습니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

한번에 바꾸는것어떨까요?? 어차피이게 마지막 피쳐개발같아서여

Copy link
Collaborator

Choose a reason for hiding this comment

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

넵넵 한번에 바꾸는게 좋을것 같습니다!

Comment on lines 95 to 115
return jpaQueryFactory
.select(new QBookQueryDto(
book.bookId,
book.title,
book.authorName,
book.publisher,
book.imageUrl,
book.isbn,
maxRoomPercentage
))
.from(room)
.join(participant).on(participant.roomJpaEntity.eq(room))
.join(participant.userJpaEntity, user)
.join(room.bookJpaEntity, book)
.where(where)
.groupBy(book.bookId)
.having(having) // 집계 함수 조건은 having 절에 넣기
.orderBy(maxRoomPercentage.desc(), book.bookId.asc()) // 방 진행도 높은 순 (내림차순), 같으면 방 아이디 작은 순 (오름차순)
.limit(pageSize + 1)
.fetch();
}
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 +19 to +45
// 저장한 책 조회시 활용
@QueryProjection
public BookQueryDto(
Long bookId,
String bookTitle,
String authorName,
String publisher,
String bookImageUrl,
String isbn,
LocalDateTime savedCreatedAt
){
this(bookId, bookTitle, authorName, publisher, bookImageUrl, isbn, savedCreatedAt,null);
}

// 활동중인 모임방 책 조회시 활용
@QueryProjection
public BookQueryDto(
Long bookId,
String bookTitle,
String authorName,
String publisher,
String bookImageUrl,
String isbn,
Double roomPercentage
){
this(bookId, bookTitle, authorName, publisher, bookImageUrl, isbn, null, roomPercentage);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

오호 생성자 오버로딩 좋습니다~

Copy link
Collaborator

Choose a reason for hiding this comment

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

LGTM bookQueryDto 좋습니다

seongjunnoh
seongjunnoh previously approved these changes Aug 30, 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.

고생하셨습니다!! 지나가는 말로 한 response 내부의 result 객체 의존까지 수정해주셨네요!!

Comment on lines +19 to +45
// 저장한 책 조회시 활용
@QueryProjection
public BookQueryDto(
Long bookId,
String bookTitle,
String authorName,
String publisher,
String bookImageUrl,
String isbn,
LocalDateTime savedCreatedAt
){
this(bookId, bookTitle, authorName, publisher, bookImageUrl, isbn, savedCreatedAt,null);
}

// 활동중인 모임방 책 조회시 활용
@QueryProjection
public BookQueryDto(
Long bookId,
String bookTitle,
String authorName,
String publisher,
String bookImageUrl,
String isbn,
Double roomPercentage
){
this(bookId, bookTitle, authorName, publisher, bookImageUrl, isbn, null, roomPercentage);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

LGTM bookQueryDto 좋습니다

// 존재하는 유저만: ACTIVE
BooleanBuilder where = new BooleanBuilder();
where.and(savedBook.userJpaEntity.userId.eq(userId));
where.and(user.status.eq(ACTIVE));
Copy link
Collaborator

Choose a reason for hiding this comment

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

filter 적용 pr에서 머지시에 삭제하겠습니다!!

수정 사유 : 중복 쿼리 발생

Comment on lines +218 to +227
// 커서 기반 추가 조건
if (userPercentageCursor != null && startDateCursor != null && roomIdCursor != null) {
where.and(participant.userPercentage.lt(userPercentageCursor)
.or(participant.userPercentage.eq(userPercentageCursor)
.and(room.startDate.gt(startDateCursor))
.or(participant.userPercentage.eq(userPercentageCursor)
.and(room.startDate.eq(startDateCursor))
.and(room.roomId.gt(roomIdCursor))
)
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

깔끔하네요 굳굳

Comment on lines 55 to 62
@AfterEach
void tearDown() {
roomParticipantJpaRepository.deleteAllInBatch();
roomJpaRepository.deleteAll();
savedBookJpaRepository.deleteAllInBatch();
bookJpaRepository.deleteAll();
userJpaRepository.deleteAll();
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

p3 : 엇 왜 트랜잭션 어노테이션 사용없이 after each + teartdown 메서드로 테스트 DB 초기화를 진행하신 건가요?? 단순 궁금증입니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

아 이거 종종 테스트코드 트랜잭션 어노테이션으로 하면 가끔식 코드 수정하고 나서 고칠일이 은근 많길래 tearDown()로하면 깔끔하게 되길래 최근에 이런방식으로 테스트코드를 짜고있습니닷

Comment on lines +135 to +142
// flush 후 책 저장일자 덮어쓰기
// 책 저장 순서 : b2 -> b1 (b1 이 가장 최신)
LocalDateTime baseTime = LocalDateTime.now();
savedBookJpaRepository.flush();
jdbcTemplate.update("UPDATE saved_books SET created_at = ? WHERE saved_id = ?",
Timestamp.valueOf(baseTime.minusMinutes(1)), sb1.getSavedId());
jdbcTemplate.update("UPDATE saved_books SET created_at = ? WHERE saved_id = ?",
Timestamp.valueOf(baseTime.minusMinutes(10)), sb2.getSavedId());
Copy link
Collaborator

Choose a reason for hiding this comment

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

굳굳

…or-book

# Conflicts:
#	src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java
#	src/main/java/konkuk/thip/book/application/port/out/BookQueryPort.java
@hd0rable hd0rable merged commit 9ca9d5a into develop Aug 30, 2025
3 of 4 checks passed
@hd0rable hd0rable deleted the refactor/#283-cursor-book branch August 30, 2025 17:50
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-326] [refactor] 누락 무한스크롤 추가 및 페이지기반 무한스크롤을 커서기반으로 수정

3 participants