[Refactor] 저장한 책, 참여중인 방의 책 조회시 커서기반 무한스크롤 추가/ [모임 홈] 진행중인 방 조회시 페이지기반 무한스크롤을 커서기반으로 수정#284
[Refactor] 저장한 책, 참여중인 방의 책 조회시 커서기반 무한스크롤 추가/ [모임 홈] 진행중인 방 조회시 페이지기반 무한스크롤을 커서기반으로 수정#284
Conversation
|
Caution Review failedThe pull request is closed. Walkthrough여러 도메인에서 페이지 기반 목록 조회를 커서 기반으로 전환하고, 저장한 책의 누락된 무한스크롤을 커서로 추가하는 등 포트/어댑터/서비스/레포지토리/컨트롤러/DTO/매퍼 및 테스트들을 커서·DTO 중심으로 변경했습니다. Changes
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)
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)
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))
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60–90 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested reviewers
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (4)
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
Test Results435 tests 435 ✅ 38s ⏱️ Results for commit bea79e5. ♻️ This comment has been updated with latest results. |
There was a problem hiding this comment.
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에서
findSavedFeedsByCreatedAt→findSavedFeedsBySavedAt으로 이름·파라미터(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만 사용 시, 동일 타임스탬프 그룹의 일부가 누락됨.Cursor에savedCreatedAt와feedId둘 다 담고, 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의
userPercentage는Double인데, 응답 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.
📒 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.of가BookPinResult반환으로 올바르게 업데이트되었으며, 테스트 코드에 관련 호출이 없어 추가 수정 불필요합니다.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: 타입 치환 일관성 👍
BookSelectableResult→BookPinResult로의 전환이 컨트롤러/서비스와 일치합니다.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.in이adapter.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: 커서 기반 응답 검증 전환 — LGTMpage 기반 기대치 제거하고 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 매칭되는지 스키마를 수동으로 확인해주세요.
src/main/java/konkuk/thip/book/adapter/in/web/response/BookSelectableListResponse.java
Show resolved
Hide resolved
src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java
Show resolved
Hide resolved
| return CursorBasedList.of(dtos, pageSize, dto -> { | ||
| Cursor nextCursor = new Cursor(List.of( | ||
| dto.roomPercentage().toString(), // 내림차순 필드, 정렬순서 1 | ||
| dto.bookId().toString() // 고유 ID, 중복 방지용 | ||
| )); | ||
| return nextCursor.toEncodedString(); | ||
| }); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Double 기반 커서 직렬화 안정성
JOINING의 roomPercentage를 Double 문자열로 커서에 넣고 eq 비교합니다. 부동소수 오차로 eq가 실패할 수 있습니다. 가능하면 BigDecimal 스케일 고정(예: 소수 1자리)로 직렬화/파싱하거나, DB 컬럼 스케일에 맞춘 문자열 포맷을 사용하세요.
src/main/java/konkuk/thip/book/adapter/out/persistence/repository/BookQueryRepository.java
Outdated
Show resolved
Hide resolved
src/main/java/konkuk/thip/book/adapter/out/persistence/repository/BookQueryRepository.java
Show resolved
Hide resolved
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepository.java
Show resolved
Hide resolved
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java
Show resolved
Hide resolved
src/main/java/konkuk/thip/room/application/mapper/RoomParticipantQueryMapper.java
Show resolved
Hide resolved
src/main/java/konkuk/thip/room/application/port/in/dto/RoomGetHomeJoinedListQuery.java
Show resolved
Hide resolved
src/main/java/konkuk/thip/room/application/service/RoomGetHomeJoinedListService.java
Show resolved
Hide resolved
There was a problem hiding this comment.
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.
📒 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
left a comment
There was a problem hiding this comment.
굿굿 좋습니다~ 테스트 코드도 꼼꼼하게 짜주셨네요! 한가지 의견 남겼는데 확인 부탁드려요
| 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); | ||
| } |
| @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(); | ||
| } |
There was a problem hiding this comment.
p3: 이거 저번에 회의에서 createdAt을 필드로 잡으면 인덱스를 걸지 않아서 조회시에 full scan을 해야 해서 성능에 안좋다고 해서 pk로 다 바꾸기로 했던 것 같은데 이 쿼리 먼저 도입해보는거 어떨까요??
한번에 다 바꾸는게 나을까요?? @seongjunnoh @hd0rable 두 분 의견에 따르겠습니다!
There was a problem hiding this comment.
한번에 바꾸는것어떨까요?? 어차피이게 마지막 피쳐개발같아서여
There was a problem hiding this comment.
넵넵 한번에 바꾸는게 좋을것 같습니다!
| 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(); | ||
| } |
| // 저장한 책 조회시 활용 | ||
| @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); | ||
| } |
There was a problem hiding this comment.
LGTM bookQueryDto 좋습니다
seongjunnoh
left a comment
There was a problem hiding this comment.
고생하셨습니다!! 지나가는 말로 한 response 내부의 result 객체 의존까지 수정해주셨네요!!
| // 저장한 책 조회시 활용 | ||
| @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); | ||
| } |
There was a problem hiding this comment.
LGTM bookQueryDto 좋습니다
| // 존재하는 유저만: ACTIVE | ||
| BooleanBuilder where = new BooleanBuilder(); | ||
| where.and(savedBook.userJpaEntity.userId.eq(userId)); | ||
| where.and(user.status.eq(ACTIVE)); |
There was a problem hiding this comment.
filter 적용 pr에서 머지시에 삭제하겠습니다!!
수정 사유 : 중복 쿼리 발생
| // 커서 기반 추가 조건 | ||
| 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)) | ||
| ) | ||
| ) |
| @AfterEach | ||
| void tearDown() { | ||
| roomParticipantJpaRepository.deleteAllInBatch(); | ||
| roomJpaRepository.deleteAll(); | ||
| savedBookJpaRepository.deleteAllInBatch(); | ||
| bookJpaRepository.deleteAll(); | ||
| userJpaRepository.deleteAll(); | ||
| } |
There was a problem hiding this comment.
p3 : 엇 왜 트랜잭션 어노테이션 사용없이 after each + teartdown 메서드로 테스트 DB 초기화를 진행하신 건가요?? 단순 궁금증입니다!
There was a problem hiding this comment.
아 이거 종종 테스트코드 트랜잭션 어노테이션으로 하면 가끔식 코드 수정하고 나서 고칠일이 은근 많길래 tearDown()로하면 깔끔하게 되길래 최근에 이런방식으로 테스트코드를 짜고있습니닷
| // 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()); |
…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
#️⃣ 연관된 이슈
📝 작업 내용
BookSelectableResult를BookPinResult로 재사용하도록 했습니다.📸 스크린샷
💬 리뷰 요구사항
📌 PR 진행 시 이러한 점들을 참고해 주세요
Summary by CodeRabbit
New Features
Enhancements
Tests