Conversation
- 커서 기반 페이지네이션에 맞게 응답 데이터 수정
- 커서 기반 페이지네이션으로 수정 - 우선순위 조건 제거 - 반환타입으로 QueryProjection 도입
- 방 검색을 위해 Projection 생성자 추가
- 함수형 인터페이스 추가로 생성 (Integer를 커서로 활용할 수 있도록)
…65) - room과 left join이 아니라, inner join을 강제할 수 있도록 (for 성능 개선)
- 카테고리, 정렬 조건 값에 따라서 분기처리 후 그에 맞는 query port 메서드 호출하도록 구현
Walkthrough방 검색을 페이지네이션에서 커서 기반으로 전환하고, “모집중 방” 전용 검색으로 API와 흐름을 재구성했다. 컨트롤러·서비스·포트·리포지토리 전반의 시그니처와 DTO가 변경되었고, 최근검색 저장 로직 연동과 스웨거 응답 코드, 테스트 케이스, JPA 제약이 갱신되었다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Client
participant Controller as RoomQueryController
participant Service as RoomSearchService
participant Port as RoomQueryPort
participant Repo as RoomQueryRepository
participant DB
Client->>Controller: GET /rooms/search (keyword, category, sort, isFinalized, cursor, userId)
Controller->>Service: searchRecruitingRooms(RoomSearchQuery)
Service->>Service: 파라미터 파싱 (sort/category/cursor)
Service->>Port: 커서 기반 검색 호출 (deadline/memberCount, with/without category)
Port->>Repo: findRecruitingRooms...(cursor, pageSize)
Repo->>DB: 키셋 쿼리(limit+1, 조건)
DB-->>Repo: List<RoomQueryDto>
Repo-->>Port: List<RoomQueryDto>
Port-->>Service: CursorBasedList(contents, nextCursor, isLast)
Service->>Service: 최근검색 저장 여부 판단 및 저장
Service->>Controller: RoomSearchResponse(roomList, nextCursor, isLast)
Controller-->>Client: BaseResponse<RoomSearchResponse>
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ 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 Results399 tests 399 ✅ 31s ⏱️ Results for commit d9d9073. |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (32)
src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java (1)
77-80: 문서 표시 순서가 불안정할 수 있음(Set.of → LinkedHashSet 생성자 패턴)
new LinkedHashSet<>(Set.of(...))는 Set.of의 순서가 보장되지 않아, 최종 표기 순서가 구현체에 의존적일 수 있습니다. 문서화의 일관성이 중요한 경우 List를 거쳐 넣는 방식을 권장합니다.적용 예:
-ROOM_SEARCH(new LinkedHashSet<>(Set.of( - INVALID_ROOM_SEARCH_SORT, - CATEGORY_NOT_MATCH -))), +ROOM_SEARCH(new LinkedHashSet<>(java.util.List.of( + INVALID_ROOM_SEARCH_SORT, + CATEGORY_NOT_MATCH +))),src/main/java/konkuk/thip/room/adapter/in/web/response/RoomSearchResponse.java (1)
11-19: deadlineDate의 문자열 직렬화 비용/일관성 고려
deadlineDate를 String으로 유지하면 포맷 일관성 관리(예: yyyy-MM-dd)와 변환 비용이 호출 지점마다 분산될 수 있습니다. 레코드에서는LocalDate로 유지하고 Jackson에 포맷을 위임하는 게 유지보수/성능 측면에서 유리합니다. 커서가LocalDate기반인 본 PR 방향성(단일 커서)과도 정합적입니다.변경 예(이 파일 내 한정):
-import java.util.List; +import java.util.List; +import java.time.LocalDate; @@ public record RoomSearchDto( Long roomId, String bookImageUrl, String roomName, int memberCount, int recruitCount, - String deadlineDate, + LocalDate deadlineDate, boolean isPublic ) {}주의: 매퍼/테스트/스웨거 포맷(@Schema, @jsonformat 등)도 함께 조정 필요.
src/main/java/konkuk/thip/room/application/port/out/dto/RoomQueryDto.java (3)
15-20: int → Integer 전환의 의도와 현재 notNull 강제 간의 불일치요약에서 "null 허용"을 위해 래퍼로 바꿨다고 하나, 실제 코드는
Assert.notNull(recruitCount),Assert.notNull(memberCount)로 null을 금지합니다. 명확한 방향성을 결정해 주세요:
- 정말 null 허용이 필요하다면: notNull 제약 제거 및 후속 사용처에서의 null 처리 보강.
- null 허용이 불필요/위험하다면: 원시형
int를 유지(또는 복귀)해 의미를 분명히 하고 불필요한 박싱/언박싱 제거.두 번째 방향(불변 보장) 예:
- Integer recruitCount, // 방 최대 인원 수 - Integer memberCount, + int recruitCount, // 방 최대 인원 수 + int memberCount, @@ - Assert.notNull(recruitCount, "recruitCount must not be null"); - Assert.notNull(memberCount, "memberCount must not be null"); + // 원시형으로 전환 시 notNull 불필요이 경우, 아래 오버로드 생성자 시그니처도 함께 원시형으로 통일 필요.
18-20: isPublic에 대한 null 가능성 명시도메인적으로 조회 유형에 따라
isPublic이 비어 있을 수 있다면,@Nullable어노테이션을 부여해 의도를 명확히 하는 것을 권장합니다(이미startDate에 적용한 패턴과 통일).- Boolean isPublic // 공개방 여부 + @Nullable Boolean isPublic // 공개방 여부
31-42: 오버로드 생성자: 의미별 명명 또는 팩토리 메서드 고려용도 주석(“내가 참여한 모임방 조회 시”)이 좋습니다. 가독성과 실수를 줄이기 위해 정적 팩토리(예:
RoomQueryDto.forJoinedRooms(...)) 도입을 검토해 주세요. 파라미터 수가 많은 오버로드는 호출부에서 혼선을 유발합니다.src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java (1)
53-54: book_id not null 제약 강화: 마이그레이션 및 인덱스 권장DB 스키마에서
book_id를 not null 로 강화한 변경은 도메인 무결성에 유익합니다. 단,
- 운영/개발 DB에 기존 null 데이터가 없는지 확인 필요(DDL 적용 실패 위험).
- 생성/테스트 픽스처(팩토리)들이 모두 Book 연관을 세팅하는지 점검 필요.
보완 제안:
- FK 인덱스 추가로 조인/필터 성능 최적화 권장.
예시(마이그레이션 SQL):
-- 선행 정합성 확보 UPDATE rooms SET book_id = <valid_id> WHERE book_id IS NULL; -- 데이터 상황에 맞게 처리 -- 제약 및 인덱스 추가 ALTER TABLE rooms ALTER COLUMN book_id SET NOT NULL; CREATE INDEX IF NOT EXISTS idx_rooms_book_id ON rooms(book_id);src/main/java/konkuk/thip/room/application/port/in/dto/RoomSearchSortParam.java (2)
21-27: 정렬 파라미터 매칭을 공백 트리밍 + 대소문자 무시로 보강하세요.외부 API 파라미터는 공백/대소문자 편차가 자주 발생합니다. equals → equalsIgnoreCase + trim 적용으로 입력 탄력성을 높이면 400 빈도와 디버깅 코스트를 줄일 수 있습니다.
- public static RoomSearchSortParam from(String value) { - return Arrays.stream(RoomSearchSortParam.values()) - .filter(param -> param.getValue().equals(value)) - .findFirst() - .orElseThrow( - () -> new InvalidStateException(INVALID_ROOM_SEARCH_SORT) - ); - } + public static RoomSearchSortParam from(String value) { + String normalized = (value == null) ? null : value.trim(); + return Arrays.stream(values()) + .filter(param -> param.getValue().equalsIgnoreCase(normalized)) + .findFirst() + .orElseThrow(() -> new InvalidStateException(INVALID_ROOM_SEARCH_SORT)); + }
15-18: RECOMMEND 정렬은 “개발 미정” 상태입니다. 입력을 받지 않거나 명시적으로 차단하는지 확인 필요.현재 값이 한글 표시 문자열이고 실제 정렬 로직이 없으면, 사용자가 전달 시 예측 불가한 결과가 나올 수 있습니다. 컨트롤러/Validator 레벨에서 해당 값은 일단 금지하거나, 임시로 명확한 대체 정렬(예: memberCount)로 매핑하는 것을 권장합니다.
src/main/java/konkuk/thip/room/application/service/RoomGetHomeJoinedListService.java (1)
56-59: 오탈자 수정 제안: “page은” → “page는”메시지 가독성을 위해 간단히 수정해 두는 편이 좋습니다.
- throw new InvalidStateException(ErrorCode.API_INVALID_PARAM, new IllegalArgumentException("page은 1 이상의 값이어야 합니다.")); + throw new InvalidStateException(ErrorCode.API_INVALID_PARAM, new IllegalArgumentException("page는 1 이상의 값이어야 합니다."));src/main/java/konkuk/thip/room/adapter/out/persistence/function/IntegerCursorRoomQueryFunction.java (2)
7-10: 커서 계약을 Javadoc으로 문서화하여 오용을 방지하세요.커서 비교의 배타성, 정렬 안정성(동점 시 id 2차 정렬), 반환 개수 한도 등 계약을 인터페이스에 명시하면 구현/테스트의 기준이 명확해집니다.
@FunctionalInterface public interface IntegerCursorRoomQueryFunction { - List<RoomQueryDto> apply(Integer lastInteger, Long lastId, int pageSize); + /** + * Integer 기반 정렬(예: memberCount) 커서 쿼리 함수. + * 계약: + * - 정렬은 결정적이어야 하며, 동점 시 id로 2차 정렬한다. + * - 커서 비교는 배타(exclusive)여야 하며, 커서에 해당하는 레코드는 결과에 포함하지 않는다. + * - 반환 크기는 pageSize를 초과하지 않는다. + * + * @param lastInteger 마지막 항목의 정렬 키 값(null이면 최초 페이지) + * @param lastId 마지막 항목의 식별자(동점 시 tie-break용, null이면 최초 페이지) + * @param pageSize 요청 페이지 크기(>0) + * @return 조회된 RoomQueryDto 목록(최대 pageSize) + */ + List<RoomQueryDto> apply(Integer lastInteger, Long lastId, int pageSize); }
1-10: 정렬 키 + id 복합 인덱스 추가로 커서 성능/안정성을 확보하세요.memberCount 기반 커서라면 DB에 (member_count DESC, id DESC) 혹은 (member_count ASC, id ASC) 복합 인덱스가 있어야 파일 정렬/소트 스필 없이 선형 접근이 가능합니다. 동일하게 deadline 정렬 경로에는 (deadline, id) 복합 인덱스가 필요합니다. 운영 트래픽에서 키워드 조건과 결합되면 스캔 범위가 커질 수 있어 인덱스 계획 점검을 권장합니다.
src/main/java/konkuk/thip/room/application/port/in/RoomSearchUseCase.java (1)
8-8: 메서드 명칭과 의미 정합성(Recruiting vs isFinalized) 점검 제안RoomSearchQuery에 isFinalized가 존재한다면 메서드명이 searchRecruitingRooms로 고정되는 것이 다소 어색할 수 있습니다. 도메인 상 “모집중만 검색”이 확정이라면 isFinalized를 제거하고 메서드명을 유지, 양자 지원이라면 메서드를 searchRooms로 완화하는 방향을 검토해 주세요.
src/main/java/konkuk/thip/room/application/mapper/RoomQueryMapper.java (1)
41-41: 메서드명 명확성: toRoomSearchResponse → toRoomSearchDtoList 권장반환 타입이 List인 만큼 메서드명이 toRoomSearchDtoList가 가독성에 더 직관적입니다. API 표면의 의미 충돌(응답 래퍼 vs DTO 리스트)을 줄일 수 있습니다.
src/main/java/konkuk/thip/room/application/port/in/dto/RoomSearchQuery.java (1)
11-14: 입력 문자열 정규화(trim, blank→null) 제안공백/빈 문자열 입력을 일관되게 처리하여 쿼리 조건 구성 시 불필요한 LIKE '%%' 생성과 인덱스 미활용을 방지할 수 있습니다. 또한 메서드명 뒤 공백 스타일도 함께 정리합니다.
다음 변경을 제안합니다:
- public static RoomSearchQuery of (String keyword, String categoryStr, String sortStr, - boolean isFinalized, String cursorStr, Long userId) { - return new RoomSearchQuery(keyword, categoryStr, sortStr, isFinalized, cursorStr, userId); - } + public static RoomSearchQuery of(String keyword, String categoryStr, String sortStr, + boolean isFinalized, String cursorStr, Long userId) { + String kw = keyword == null ? null : keyword.strip(); + String cat = categoryStr == null ? null : categoryStr.strip(); + String sort = sortStr == null ? null : sortStr.strip(); + String cur = cursorStr == null ? null : cursorStr.strip(); + // 빈 문자열을 null로 승격 (검색 조건 최적화) + if (kw != null && kw.isEmpty()) kw = null; + if (cat != null && cat.isEmpty()) cat = null; + if (sort != null && sort.isEmpty()) sort = null; + if (cur != null && cur.isEmpty()) cur = null; + return new RoomSearchQuery(kw, cat, sort, isFinalized, cur, userId); + }src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
159-170: createCustomRoom 오버로드 간 recruitCount 상이(3 vs 20) — 의도 명시 권장정렬(신청 인원) 관련 테스트를 위한 의도라면 주석으로 명시해 혼란을 줄이는 것이 좋습니다. 메서드명으로 의도를 드러내는 것도 한 방법입니다.
아래처럼 Javadoc을 추가해 의도를 문서화하는 것을 제안합니다:
+ /** + * 방 이름을 커스텀으로 지정하는 팩토리. + * 주의: 정렬(신청 인원) 관련 테스트 편의를 위해 recruitCount 기본값을 20으로 설정합니다. + */ public static RoomJpaEntity createCustomRoom(BookJpaEntity book, CategoryJpaEntity category, String roomName, LocalDate startDate, LocalDate endDate) { return RoomJpaEntity.builder() .title(roomName) .description("설명") .isPublic(true) .startDate(startDate) .endDate(endDate) .recruitCount(20) .bookJpaEntity(book) .categoryJpaEntity(category) .build(); }추가로, 장기적으로는 recruitCount를 파라미터화한 createRoomWithRecruitCount(...) 도 고려해볼 수 있습니다.
Also applies to: 172-183
src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java (1)
20-27: 커서 페이지네이션 안정 정렬 키와 커서 구성 명시 권장memberCount/deadline 정렬 시 동순위가 빈번합니다. 커서 기반 페이지네이션에서 안정 정렬(secondary key) 미지정 시 중복/누락이 발생할 수 있습니다. 인터페이스 레벨에서 정렬 키와 커서 구성 원칙을 Javadoc으로 명시해 구현과 테스트 일관성을 확보해 주세요.
다음처럼 주석을 보강하는 것을 제안합니다:
- /** - * 방 검색 - */ + /** + * 방 검색 (커서 기반) + * 정렬 및 커서 구성 원칙: + * - deadline 정렬: endDate ASC(또는 D-day ASC) + id DESC 보조 정렬을 적용. 커서는 (endDate, id) 기반으로 구성. + * - memberCount 정렬: memberCount DESC + id DESC 보조 정렬을 적용. 커서는 (memberCount, id) 기반으로 구성. + * - keyword가 비어있는 경우 LIKE 조건을 생략하여 인덱스 활용 극대화. + */ CursorBasedList<RoomQueryDto> searchRecruitingRoomsByDeadline(String keyword, Cursor cursor); CursorBasedList<RoomQueryDto> searchRecruitingRoomsWithCategoryByDeadline(String keyword, Category category, Cursor cursor); CursorBasedList<RoomQueryDto> searchRecruitingRoomsByMemberCount(String keyword, Cursor cursor); CursorBasedList<RoomQueryDto> searchRecruitingRoomsWithCategoryByMemberCount(String keyword, Category category, Cursor cursor);또한 4개 메서드를 하나의 파라미터 객체(예: SearchCondition[sortType, categoryOpt])로 통합하는 리팩토링은 선택 사항이지만, 중복을 줄이고 확장성(정렬 기준 추가)에 유리합니다.
src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java (3)
42-43: nullable 스펙 정합 및 퍼포먼스 관점에서 defaultValue 제거 제안PR 목적에 따라 category는 nullable입니다. defaultValue=""는 빈 문자열을 만들어 조건 처리 분기를 어렵게 하고, LIKE '%%' 조건 생성으로 인덱스 미활용을 유발할 수 있습니다. required=false만 두고 null을 서비스에서 의미 있게 처리하는 편이 안전합니다.
- @Parameter(description = "검색 키워드 (책 이름 or 방 이름)", example = "해리") @RequestParam(value = "keyword", required = false, defaultValue = "") final String keyword, - @Parameter(description = "모임방 카테고리", example = "문학") @RequestParam(value = "category", required = false, defaultValue = "") final String category, + @Parameter(description = "검색 키워드 (책 이름 or 방 이름)", example = "해리") @RequestParam(value = "keyword", required = false) final String keyword, + @Parameter(description = "모임방 카테고리", example = "문학") @RequestParam(value = "category", required = false) final String category,참고: RoomSearchQuery.of(...)에서 trim/blank→null 정규화를 적용하면(별도 코멘트) 조건 구성 최적화에 더 효과적입니다.
44-44: 정렬 파라미터 허용값 문서화(스웨거) 권장sort는 deadline/memberCount 두 값만 허용되는 것으로 보입니다. OpenAPI 문서에 허용값을 명시하세요. 런타임 검증은 유스케이스에서 처리하되, 문서화는 컨트롤러에서 충분합니다.
- @Parameter(description = "정렬 방식 (마감 임박 : deadline, 신청 인원 : memberCount)", example = "deadline") @RequestParam("sort") final String sort, + @Parameter( + description = "정렬 방식 (마감 임박 : deadline, 신청 인원 : memberCount)", + example = "deadline", + schema = @io.swagger.v3.oas.annotations.media.Schema(allowableValues = {"deadline", "memberCount"}) + ) + @RequestParam("sort") final String sort,추가 import 필요:
import io.swagger.v3.oas.annotations.media.Schema;
41-53: 빈 키워드 처리 시 LIKE '%%'로 인한 풀스캔 가능성 — 조건 생략 처리 권장QueryDSL에서 containsIgnoreCase("")는 실질적으로 LIKE '%%'를 생성해 대규모 풀스캔/필터 비용을 유발할 수 있습니다. 키워드가 null/blank이면 제목/책제목 조건을 아예 추가하지 않도록 서비스/리포지토리에서 분기 처리해 주세요. PR 본문 TODO로 남기신 부분의 정식 반영을 제안합니다. RoomSearchQuery.of(...)에서 blank→null 정규화를 함께 적용하면 구현이 단순해집니다.
src/test/java/konkuk/thip/room/adapter/in/web/RoomSearchApiTest.java (6)
81-86: JDBC로 직접 member_count 업데이트 시 1차 캐시와의 불일치 가능성 주의테스트 흐름상 HTTP 요청이 별도 트랜잭션에서 실행되어 문제될 가능성은 낮지만, 같은 트랜잭션/영속성 컨텍스트에서 엔티티를 다시 읽는 경우 JPA 1차 캐시로 인해 값 불일치가 생길 수 있습니다. 안정성을 높이려면 업데이트 후 EntityManager clear/flush를 하거나, 테스트 전용 업데이트 메서드를 리포지토리에 추가해 JPA 경로로 갱신하는 것을 고려해보세요.
163-187: 인기순(=memberCount DESC) 정렬 검증 좋습니다 — tie-case 추가 제안현재는 서로 다른 memberCount만 존재합니다. 동일 memberCount 간의 roomId ASC 타이브레이커를 보장하는 추가 케이스를 하나 포함하면 키셋 커서 조건의 안정성을 더 확실히 검증할 수 있습니다.
원하는 경우 tie-case를 포함한 추가 테스트 메서드 초안도 제공해드릴게요.
250-261: 키워드/카테고리 미지정 전체 조회 케이스 적절기본 pageSize(10)에 의존하는 테스트가 여럿 존재합니다. 서비스의 DEFAULT_PAGE_SIZE 변경 시 테스트가 깨질 수 있으므로, 가능하다면 테스트 상수로 추출하거나 설정값을 주입받아 사용하도록 리팩토링을 고려할 수 있습니다.
301-332: finalized=true 시 최근 검색 저장 검증이 명확합니다유저/키워드 매핑과 저장 여부를 정확히 검증하고 있습니다. 추가로, 중복 저장 정책(동일 키워드 재검색 시 갱신/중복 여부)을 정의했다면 해당 시나리오 테스트도 보강하면 좋습니다.
334-398: 커서 기반 페이지네이션 테스트가 좋습니다—2페이지 말단(isLast=true)까지 확인첫 페이지 nextCursor 추출/두 번째 요청으로 이어지는 플로우가 깔끔합니다. 두 번째 응답에서 nextCursor가 null/미존재임을 함께 검증하면 API 명세를 더 엄격히 보장할 수 있습니다.
다음과 같은 한 줄을 추가해볼 수 있습니다:
- .andExpect(jsonPath("$.data.nextCursor").doesNotExist())
336-336: 테스트 메서드명 오탈자/의미 불일치메서드명이 comment_show_all_page_test로 되어 있습니다. 방 검색 페이지 테스트 성격에 맞게 room_search_pagination_test 등으로 변경하면 가독성이 좋아집니다.
src/main/java/konkuk/thip/room/application/service/RoomSearchService.java (3)
31-51: searchRecruitingRooms 전체 플로우 구성 깔끔 — 최근검색 저장 호출 조건 최적화 제안isFinalized 여부와 무관하게 매번 recentSearchCreateManager를 호출하고 내부에서 분기하는 형태로 보입니다. 호출 자체를 if (query.isFinalized())로 감싸면 불필요한 호출을 줄이고 의도가 더 명확해집니다.
적용 예:
- recentSearchCreateManager.saveRecentSearchByUser(query.userId(), query.keyword(), RecentSearchType.ROOM_SEARCH, query.isFinalized()); + if (query.isFinalized()) { + recentSearchCreateManager.saveRecentSearchByUser(query.userId(), query.keyword(), RecentSearchType.ROOM_SEARCH, true); + }
53-71: executeRecruitingRoomSearch: 불필요한 지역변수 제거 및 default 방어 구문 권고result 지역변수는 사용되지 않습니다. 또한 switch에 default가 없는데, RoomSearchSortParam.from(...)에서 보장된다면 문제 없지만, 방어적으로 IllegalArgumentException을 던지면 디버깅에 유리합니다.
간단화 예:
- private CursorBasedList<RoomQueryDto> executeRecruitingRoomSearch(... ) { - CursorBasedList<RoomQueryDto> result = null; - if (category == null) { - switch (sortParam) { - case DEADLINE: return ...; - case MEMBER_COUNT: return ...; - } - } else { - switch (sortParam) { - case DEADLINE: return ...; - case MEMBER_COUNT: return ...; - } - } - return result; - } + private CursorBasedList<RoomQueryDto> executeRecruitingRoomSearch(... ) { + if (category == null) { + return switch (sortParam) { + case DEADLINE -> roomQueryPort.searchRecruitingRoomsByDeadline(query.keyword(), cursor); + case MEMBER_COUNT -> roomQueryPort.searchRecruitingRoomsByMemberCount(query.keyword(), cursor); + }; + } else { + return switch (sortParam) { + case DEADLINE -> roomQueryPort.searchRecruitingRoomsWithCategoryByDeadline(query.keyword(), category, cursor); + case MEMBER_COUNT -> roomQueryPort.searchRecruitingRoomsWithCategoryByMemberCount(query.keyword(), category, cursor); + }; + } + }
23-24: DEFAULT_PAGE_SIZE 하드코딩 의존성 축소 제안테스트도 10개 페이지 크기에 의존하고 있어 상수 변경 시 파급이 큽니다. 설정값(application.yml)으로 분리해 주입받거나, 공용 상수 클래스로 옮겨 테스트가 동일 상수를 참조하도록 하면 안전합니다.
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java (4)
57-65: LIKE %keyword% 검색의 비효율성(TODO) — 구체적 개선 옵션 제안현재 containsIgnoreCase는 비인덱스 풀스캔을 유발합니다. DB에 따라 다음을 검토해보세요.
- PostgreSQL: pg_trgm + GIN 인덱스, ILIKE '%...%' 가속
- MySQL/InnoDB: FULLTEXT INDEX + MATCH AGAINST (자연어/Boolean 모드)
- 접두(prefix) 매칭 우선 + 넓은 매칭 보조(두 단계 쿼리)로 selectivity 확보
- 키워드 정규화(공백/특수문자 제거, 길이 제한) 및 다중 토큰 AND 매칭
현 PR 범위에서는 TODO 유지가 합리적이지만, 다음 스텝에서 성능 튜닝 가이드를 준비해두면 좋겠습니다.
99-115: 모집중(마감임박순) 키셋 로직 일관성 OKwhere, orderBy, limit(pageSize+1) 구성과 커서 조건의 정합성이 좋습니다. LocalDate.now() 사용은 간단하지만, 테스트 안정성을 위해 주입형 Clock 또는 today 인자를 사용하는 구조도 고려해볼 수 있습니다.
206-271: 홈 참여 방 조회 매핑 변경(Page) 타당 — 진행률 반올림 처리 확인userPercentage 반올림(Math.round) → int 변환 로직이 명확합니다. 혹시 소수점 한 자리 반올림 등 UI 요구가 달라질 수 있으니, 매핑 수준에서 utility로 분리해 재사용/테스트 가능하게 만드는 것도 한 방법입니다.
449-488: 공통 커서 + 2단계 조회(fetchMyRooms) 구조 재사용성 우수ascending/descending 분기와 커서 조건이 명확합니다. 여기도 상단에서 언급한 today/Clock 주입 패턴을 고려하면 테스트 안정성이 더 좋아집니다.
📜 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 (20)
src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java(1 hunks)src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java(2 hunks)src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetHomeJoinedListResponse.java(2 hunks)src/main/java/konkuk/thip/room/adapter/in/web/response/RoomSearchResponse.java(1 hunks)src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java(1 hunks)src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java(3 hunks)src/main/java/konkuk/thip/room/adapter/out/persistence/function/IntegerCursorRoomQueryFunction.java(1 hunks)src/main/java/konkuk/thip/room/adapter/out/persistence/function/LocalDateCursorRoomQueryFunction.java(1 hunks)src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepository.java(1 hunks)src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java(7 hunks)src/main/java/konkuk/thip/room/application/mapper/RoomQueryMapper.java(2 hunks)src/main/java/konkuk/thip/room/application/port/in/RoomSearchUseCase.java(1 hunks)src/main/java/konkuk/thip/room/application/port/in/dto/RoomSearchQuery.java(1 hunks)src/main/java/konkuk/thip/room/application/port/in/dto/RoomSearchSortParam.java(2 hunks)src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java(1 hunks)src/main/java/konkuk/thip/room/application/port/out/dto/RoomQueryDto.java(1 hunks)src/main/java/konkuk/thip/room/application/service/RoomGetHomeJoinedListService.java(1 hunks)src/main/java/konkuk/thip/room/application/service/RoomSearchService.java(1 hunks)src/test/java/konkuk/thip/common/util/TestEntityFactory.java(4 hunks)src/test/java/konkuk/thip/room/adapter/in/web/RoomSearchApiTest.java(5 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#113
File: src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java:38-44
Timestamp: 2025-07-30T14:05:04.945Z
Learning: seongjunnoh는 코드 최적화 제안에 대해 구체적인 기술적 근거와 효율성 차이를 이해하고 싶어하며, 성능 개선 방식에 대한 상세한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:49-114
Timestamp: 2025-07-28T16:44:31.224Z
Learning: seongjunnoh는 코드 중복 문제에 대한 리팩토링 제안을 적극적으로 수용하고 함수형 인터페이스를 활용한 해결책을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#195
File: src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java:0-0
Timestamp: 2025-08-13T05:22:32.287Z
Learning: seongjunnoh는 데이터 무결성과 중복 방지에 대한 고민이 깊으며, LinkedHashSet을 활용한 중복 제거와 순서 보장을 동시에 달성하는 솔루션을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/application/service/AttendanceCheckCreateService.java:22-40
Timestamp: 2025-08-14T09:15:31.332Z
Learning: seongjunnoh는 기술적 완벽성보다 실제 비즈니스 시나리오와 사용자 행동 패턴을 우선 고려하며, 발생 가능성이 낮은 엣지 케이스보다는 실용적인 구현을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#112
File: src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java:272-272
Timestamp: 2025-07-30T10:44:34.115Z
Learning: seongjunnoh는 피드 커서 페이지네이션에서 LocalDateTime 단일 커서 방식을 선호하며, 복합 키 기반 커서보다 구현 단순성과 성능을 우선시한다.
🧬 Code Graph Analysis (1)
src/test/java/konkuk/thip/room/adapter/in/web/RoomSearchApiTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
TestEntityFactory(33-417)
🔇 Additional comments (29)
src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java (1)
77-80: ROOM_SEARCH에 CATEGORY_NOT_MATCH 추가 취지 적합카테고리 검증 로직이 추가된 흐름과 문서가 일치합니다. 별도 이슈 없어 보입니다.
src/main/java/konkuk/thip/room/adapter/in/web/response/RoomSearchResponse.java (1)
6-9: 커서 기반 응답isLast필드 노출 일관 확인 완료
JSONPath 테스트($.data.isLast)와 코드 전반에서boolean isLast를 참조하고 있어 Jackson 노출 값이 의도한 대로isLast로 일치함을 확인했습니다.
- RoomSearchResponse, RoomShowMineResponse 등 커서 기반 응답 클래스 필드
isLast사용- RoomSearchApiTest 등에서
.andExpect(jsonPath("$.data.isLast", …))검증Swagger 스펙 및 프론트엔드에서도 동일하게
isLast로 노출되는지 최종 확인만 부탁드립니다.src/main/java/konkuk/thip/room/application/port/out/dto/RoomQueryDto.java (1)
44-56: 쿼리 프로젝션의 bookImageUrl은 DB 스키마에서 항상 NOT NULL
BookJpaEntity.image_url컬럼이nullable = false로 정의되어 있어, 쿼리 프로젝션(RoomQueryRepositoryImpl의new QRoomQueryDto(...))에서 넘어오는book.imageUrl값이 null일 수 없습니다.
따라서 해당 생성자의Assert.notNull(bookImageUrl)전제는 안전하며, 별도의 기본값 대체나@Nullable허용이 필요하지 않습니다.src/main/java/konkuk/thip/room/adapter/out/persistence/function/LocalDateCursorRoomQueryFunction.java (1)
9-11: LGTM: 인터페이스 명시성 개선 완료
RoomQueryFunction잔존 참조가 모두 제거된 것을 확인했습니다.LocalDateCursorRoomQueryFunction및IntegerCursorRoomQueryFunction이RoomQueryPersistenceAdapter.java에서 정상적으로 사용되고 있습니다.- 향후 제네릭 커서 함수 통합 여부는 선택 사항입니다.
src/main/java/konkuk/thip/room/application/port/in/dto/RoomSearchSortParam.java (1)
15-16: 커서 기반 정렬의 안정성 보장(secondary key tie-break)이 구현과 일치하는지 확인하세요.DEADLINE, MEMBER_COUNT 정렬 모두에서 중복/누락을 피하려면 정렬 키 동률 시 id로 2차 정렬하고, 커서 비교는 “엄격 배타(exclusive)” 조건을 사용해야 합니다. 저장소 구현과 정렬/커서 비교 규칙이 Enum 값과 일치하는지 점검 부탁드립니다.
src/main/java/konkuk/thip/room/application/service/RoomGetHomeJoinedListService.java (2)
43-43: 타입 정합성 변경 LGTMPage로의 전환이 상·하위 레이어 시그니처와 일관되며, DTO 필드 구조도 그대로 유지되어 안전합니다.
36-41: 유저 조회 예외 처리와 size 의미를 확인해 주세요.
- 유저 닉네임 조회: findById가 존재하지 않을 때 도메인 예외를 던지는지 확인이 필요합니다. null 반환이라면 NPE 위험이 있습니다. 조회 전용 포트 분리(UserQueryPort)도 고려해볼 수 있습니다.
- size 필드: 현재 페이지의 실제 요소 수(NumberOfElements)를 노출합니다. API 계약이 “요청 pageSize”를 기대하는지 “응답에 포함된 개수”를 기대하는지 확인 부탁드립니다. 후자면 그대로 OK, 전자면 DEFAULT_PAGE_SIZE를 내려야 합니다.
- pageIndex 계산: validatePage에서 page ≥ 1을 보장하므로 삼항의 else 경로는 실행되지 않습니다. 간소화 여지가 있습니다.
Also applies to: 47-53
src/main/java/konkuk/thip/room/application/port/in/RoomSearchUseCase.java (1)
8-8: 레거시 메서드 호출(searchRoom) 제거 확인 완료
검증 스크립트 결과:
searchRoom(...)호출 흔적 없음- 컨트롤러 및 서비스에서 모두
searchRecruitingRooms(...)시그니처 사용RoomSearchQuery.of(...)를 통한 쿼리 객체 생성 로직 정상 반영추가 조치가 필요 없습니다.
src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetHomeJoinedListResponse.java (1)
9-9: DTO 타입 리네이밍 일관성 LGTMroomList 제네릭이 JoinedRoomInfo로 일원화되어 서비스/리포지토리 시그니처와 일치합니다. JSON 스키마에도 영향이 없어 보입니다.
src/main/java/konkuk/thip/room/application/mapper/RoomQueryMapper.java (2)
37-39: null-safe 매핑과 포맷팅 사용 LGTMdeadlineDate 포맷팅과 isPublic의 null-safe 처리(Boolean.TRUE.equals(...))가 명확하고 안전합니다. MapStruct import 설정도 적절합니다.
37-41: 핵심 DTO 필드 자동 매핑 일치 확인
- roomId, bookImageUrl, roomName, recruitCount, memberCount: 이름 기반 자동 매핑 (Integer→int 언박싱)
- endDate → deadlineDate, isPublic → isPublic: 표현식(java(DateUtil.formatAfterTime(…)), java(Boolean.TRUE.equals(…)))으로 처리
- startDate: 대상 DTO에 미포함되어 unmappedTargetPolicy=IGNORE에 따라 의도적으로 제외
필드명이 모두 정확히 일치하며 매핑 로직이 기대대로 동작함을 확인했습니다.
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
120-131: 테스트 가독성을 높이는 Book 팩토리 추가 LGTM제목만 바꾸는 팩토리 메서드는 검색 시나리오 테스트에 유용합니다. pageCount=300 설정도 도메인 제약과 충돌하지 않는 한 문제 없습니다.
src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java (2)
30-31: 홈 참여 방 응답 타입 변경 LGTMJoinedRoomInfo로의 전환은 표현 계층 의존도를 낮추고 응답 도메인을 명확히 합니다.
23-26: memberCount 정렬에 대한 보조 정렬(ID) 적용 확인됨RoomQueryRepositoryImpl.java 에서 아래와 같이 memberCount 정렬 시
room.roomId.asc()가 적용되어 동순위 처리 이슈가 해소된 것을 확인했습니다.
- …/RoomQueryRepositoryImpl.java:151
.orderBy(room.memberCount.desc(), room.roomId.asc())- …/RoomQueryRepositoryImpl.java:171
.orderBy(room.memberCount.desc(), room.roomId.asc())다만, deadline(endDate) 기반 검색 (
searchRecruitingRoomsByDeadline,searchRecruitingRoomsWithCategoryByDeadline) 구현에서 id 보조 정렬 및 커서 비교 조건이 적용됐는지는 아직 확인되지 않습니다. 아래 스크립트로 추가 검토 부탁드립니다:# deadline 정렬 및 보조키(id) 확인 rg -n -C3 -P 'orderBy\([^)]*endDate[^)]*\)' --type java rg -n -C3 -P '(endDate).*(>|<|>=|<=|,).*id' --type java미적용 시
orderBy(room.endDate.asc(), room.roomId.asc())및 커서 비교 로직에endDate+roomId조합 조건을 추가해주세요.src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java (1)
41-53: 확정 입력(isFinalized=true) 시 최근 검색어 저장 로직 정상 동작 확인
- RoomSearchService.searchRecruitingRooms()에서
query.isFinalized()값을 RecentSearchCreateManager.saveRecentSearchByUser(...)에 전달- RecentSearchCreateManager 내부에서
if (isFinalized)조건으로만 저장 또는 갱신 수행서비스 계층에서 요구사항대로 조건부 저장이 이미 구현되어 있으므로 추가 작업이 필요 없습니다.
src/test/java/konkuk/thip/room/adapter/in/web/RoomSearchApiTest.java (3)
117-131: 정렬 검증과 타이브레이커(roomId 오름차순) 검증 케이스가 명확합니다keyword+마감임박순 조합에 대한 예상 결과와 isLast=true 검증이 명확합니다. 타이브레이커(roomId ASC)도 코멘트로 명시하여 유지보수에 도움 됩니다.
214-224: 카테고리 필터 + 마감임박순 시나리오 OK카테고리 값과 기대 결과가 일치합니다. 커서 기반이지만 단일 페이지(isLast=true) 검증으로 충분합니다.
287-299: 키워드+카테고리 교집합 필터링 검증 LGTM교집합 조건만 반환되는지, 정렬까지 일관적으로 확인되어 좋습니다.
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepository.java (2)
15-21: 커서 기반 메서드 시그니처 분리 설계 적절정렬 기준(마감임박/인기) 및 카테고리 유무를 메서드로 분리한 것은 구현과 최적화 포인트를 명확히 해주어 가독성과 유지보수성이 좋습니다.
25-25: 홈 참여 방 조회 반환타입 변경(Page) 일관성 확보응답 타입을 JoinedRoomInfo로 명확히 한 변경은 상위 레이어에서의 DTO 혼동을 줄여줍니다. 관련 매퍼/컨트롤러 연쇄 변경이 모두 반영되었는지 한번 더 확인 부탁드립니다.
src/main/java/konkuk/thip/room/application/service/RoomSearchService.java (1)
73-79: 카테고리 검증 단순화 LGTMnull/empty 처리 후 Enum 변환으로 충분하고, 잘못된 값은 Category.from에서 책임지도록 둔 것도 적절합니다.
src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java (3)
127-141: memberCount 기반 커서 구성은 올바릅니다DESC 정렬에서 (memberCount < lastMemberCount) OR (memberCount == lastMemberCount AND roomId > lastId) 조건과 호응하도록 커서를 구성해 안정적입니다.
37-41: 레포지토리 위임 람다 구성 깔끔Keyset 래퍼(findRoomsByDeadlineCursor)를 통한 위임 형태가 재사용성과 가독성이 좋습니다.
106-110: ISBN 기반 조회도 동일 래퍼 사용으로 일관성 확보deadline 커서 래퍼 재사용이 일관성을 높입니다. 상단 코멘트(startDate/endDate 명칭 통일)만 반영되면 전반적으로 안정적입니다.
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java (5)
66-73: startDate ASC 커서 조건식 정확(> lastStartDate) OR (= lastStartDate AND roomId > lastId) 형태로 타이브레이커 정렬(roomId ASC)과 일치합니다.
82-93: 공통 프로젝션 도입으로 중복 제거 — 좋습니다QRoomQueryDto 프로젝션 메서드로 SELECT 중복을 줄인 점이 좋습니다. 다만 다른 메서드(카테고리별 추천, ISBN 등)에서도 동일 필드 구성을 유지할 필요가 있습니다.
139-154: 인기순 키셋 로직 일관성 OKDESC 정렬과 커서 조건이 잘 맞물립니다.
358-375: 카테고리 기반 추천 쿼리 inner join 변경 좋음 — 프로젝션 인자 수 일관성 확인여기와 다른 몇몇 메서드는 QRoomQueryDto 생성자 인자 수가 6개(마지막이 startDate)인데, 위의 공통 프로젝션은 isPublic까지 7개를 제공합니다. RoomQueryDto에 @QueryProjection 생성자가 복수로 존재한다면 문제없지만, 아니면 컴파일 타임 불일치가 생길 수 있어 확인이 필요합니다.
426-444: userJoinedRoom 서브쿼리로 미참여 방만 필터 처리 👍추천 쿼리의 의도를 잘 드러냅니다. 적절한 인덱스(user_id, room_id)가 보장된다면 성능도 무난합니다.
| summary = "모집중인 방 검색", | ||
| description = "검색어(= 방이름 or 첵제목), 카테고리 와 매핑되는 모집중인 방을 검색합니다. 공개/비공개 방 모두 검색 가능합니다." | ||
| ) |
There was a problem hiding this comment.
오탈자 수정: ‘첵제목’ → ‘책제목’
사용자 문서에 노출되는 스웨거 설명의 오탈자를 바로잡아 주세요.
적용 diff:
- description = "검색어(= 방이름 or 첵제목), 카테고리 와 매핑되는 모집중인 방을 검색합니다. 공개/비공개 방 모두 검색 가능합니다."
+ description = "검색어(= 방이름 or 책제목), 카테고리와 매핑되는 모집중인 방을 검색합니다. 공개/비공개 방 모두 검색 가능합니다."📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| summary = "모집중인 방 검색", | |
| description = "검색어(= 방이름 or 첵제목), 카테고리 와 매핑되는 모집중인 방을 검색합니다. 공개/비공개 방 모두 검색 가능합니다." | |
| ) | |
| summary = "모집중인 방 검색", | |
| description = "검색어(= 방이름 or 책제목), 카테고리와 매핑되는 모집중인 방을 검색합니다. 공개/비공개 방 모두 검색 가능합니다." | |
| ) |
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java around
lines 36 to 38, fix the typo in the Swagger description by replacing "첵제목" with
"책제목" so the description reads that the search uses the room name or 책제목; update
the string literal accordingly and keep formatting/escaping consistent with
surrounding annotations.
There was a problem hiding this comment.
@buzz0331, 감사합니다! 사용자에게 노출되는 문서의 품질이 중요하다고 생각합니다. 😊
| private CursorBasedList<RoomQueryDto> findRoomsByDeadlineCursor(Cursor cursor, LocalDateCursorRoomQueryFunction queryFunction) { | ||
| LocalDate lastLocalDate = cursor.isFirstRequest() ? null : cursor.getLocalDate(0); | ||
| Long lastId = cursor.isFirstRequest() ? null : cursor.getLong(1); | ||
| int pageSize = cursor.getPageSize(); | ||
|
|
||
| List<RoomQueryDto> dtos = queryFunction.apply(lastLocalDate, lastId, pageSize); | ||
|
|
||
| return CursorBasedList.of(dtos, pageSize, roomShowMineQueryDto -> { | ||
| return CursorBasedList.of(dtos, pageSize, dto -> { | ||
| Cursor nextCursor = new Cursor(List.of( | ||
| dto.endDate().toString(), | ||
| dto.roomId().toString() | ||
| )); | ||
| return nextCursor.toEncodedString(); | ||
| }); | ||
| } |
There was a problem hiding this comment.
커서 구성 시 startDate/“endDate” 필드 혼용 가능성 — 불일치로 인한 버그 위험
키셋 정렬은 startDate ASC + roomId ASC 기준인데, nextCursor는 dto.endDate()를 사용하고 있습니다. 현재 RoomQueryRepositoryImpl에서는 projection으로 room.startDate를 선택합니다. DTO 접근자가 endDate라는 이름으로 startDate를 담고 있다면 동작은 하지만, 네이밍 불일치가 유지보수 시 심각한 혼동/버그를 유발할 수 있습니다.
정렬 기준과 동일하게 startDate 접근자를 사용하도록 정리하는 것을 권장합니다.
다음과 같이 수정하는 것을 제안합니다(실제 접근자 명은 DTO 정의에 맞춰 조정 필요):
- dto.endDate().toString(),
+ dto.startDate().toString(),또한 RoomQueryDto/QRoomQueryDto의 필드/생성자 명세에서 startDate/endDate 명칭을 정합성 있게 통일해주세요.
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java
around lines 111 to 125, the cursor is being built using dto.endDate() while the
keyset sort and repository projection use startDate, causing a naming/behavior
mismatch; change the cursor to use the DTO's startDate accessor (e.g.,
dto.startDate().toString()) so the cursor keys match the sort order, and update
RoomQueryDto/QRoomQueryDto field/accessor/constructor names to consistently use
startDate (or rename repository projection to endDate everywhere) to keep names
and semantics aligned across query, DTO and cursor generation.
There was a problem hiding this comment.
이건 나도 동감해 그래서 dto 변수 네이밍을 수정할 예정이야
- 방 활동 시작일
- 방 모집 마감일
- 방 활동 마감일
이렇게 수정할 예정이야
There was a problem hiding this comment.
@seongjunnoh 네, 정말 좋은 개선 계획이네요!
더 의미가 명확한 네이밍으로 변경하시면 코드 가독성과 유지보수성이 크게 향상될 것 같습니다:
- 방 활동 시작일 - 실제 독서 활동이 시작되는 날짜
- 방 모집 마감일 - 참가자 모집이 끝나는 날짜
- 방 활동 마감일 - 독서 활동이 완전히 종료되는 날짜
이렇게 하면 커서 생성 시에도 dto.방활동시작일() 같은 형태로 정렬 기준과 일치하는 필드를 명확하게 참조할 수 있어서 혼동의 여지가 없어지겠네요.
🐰
┌─────┐
│ 👍 │ Good plan!
└─────┘
🧠 Learnings used
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#57
File: src/test/java/konkuk/thip/room/domain/RoomTest.java:0-0
Timestamp: 2025-07-08T16:30:33.771Z
Learning: Room 도메인에서 startDate는 현재 날짜 이후여야 하는 도메인 규칙이 있어서, 테스트에서 만료된 상태를 시뮬레이션하려면 reflection을 사용해야 한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#113
File: src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java:38-44
Timestamp: 2025-07-30T14:05:04.945Z
Learning: seongjunnoh는 코드 최적화 제안에 대해 구체적인 기술적 근거와 효율성 차이를 이해하고 싶어하며, 성능 개선 방식에 대한 상세한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:49-114
Timestamp: 2025-07-28T16:44:31.224Z
Learning: seongjunnoh는 코드 중복 문제에 대한 리팩토링 제안을 적극적으로 수용하고 함수형 인터페이스를 활용한 해결책을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#195
File: src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java:0-0
Timestamp: 2025-08-13T05:22:32.287Z
Learning: seongjunnoh는 데이터 무결성과 중복 방지에 대한 고민이 깊으며, LinkedHashSet을 활용한 중복 제거와 순서 보장을 동시에 달성하는 솔루션을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/application/service/AttendanceCheckCreateService.java:22-40
Timestamp: 2025-08-14T09:15:31.332Z
Learning: seongjunnoh는 기술적 완벽성보다 실제 비즈니스 시나리오와 사용자 행동 패턴을 우선 고려하며, 발생 가능성이 낮은 엣지 케이스보다는 실용적인 구현을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#112
File: src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java:272-272
Timestamp: 2025-07-30T10:44:34.115Z
Learning: seongjunnoh는 피드 커서 페이지네이션에서 LocalDateTime 단일 커서 방식을 선호하며, 복합 키 기반 커서보다 구현 단순성과 성능을 우선시한다.
buzz0331
left a comment
There was a problem hiding this comment.
굿굿 수고하셨습니다~~~ 이제 조회 로직에 틀이 잡혀서 그런지 코드가 엄청 깔끔한 것 같네요!!
| private CursorBasedList<RoomQueryDto> findRoomsByDeadlineCursor(Cursor cursor, LocalDateCursorRoomQueryFunction queryFunction) { | ||
| LocalDate lastLocalDate = cursor.isFirstRequest() ? null : cursor.getLocalDate(0); | ||
| Long lastId = cursor.isFirstRequest() ? null : cursor.getLong(1); | ||
| int pageSize = cursor.getPageSize(); | ||
|
|
||
| List<RoomQueryDto> dtos = queryFunction.apply(lastLocalDate, lastId, pageSize); | ||
|
|
||
| return CursorBasedList.of(dtos, pageSize, roomShowMineQueryDto -> { | ||
| return CursorBasedList.of(dtos, pageSize, dto -> { | ||
| Cursor nextCursor = new Cursor(List.of( | ||
| dto.endDate().toString(), | ||
| dto.roomId().toString() | ||
| )); | ||
| return nextCursor.toEncodedString(); | ||
| }); | ||
| } | ||
|
|
||
| private CursorBasedList<RoomQueryDto> findRoomsByMemberCountCursor(Cursor cursor, IntegerCursorRoomQueryFunction queryFunction) { | ||
| Integer lastInteger = cursor.isFirstRequest() ? null : cursor.getInteger(0); | ||
| Long lastId = cursor.isFirstRequest() ? null : cursor.getLong(1); | ||
| int pageSize = cursor.getPageSize(); | ||
|
|
||
| List<RoomQueryDto> dtos = queryFunction.apply(lastInteger, lastId, pageSize); | ||
|
|
||
| return CursorBasedList.of(dtos, pageSize, dto -> { | ||
| Cursor nextCursor = new Cursor(List.of( | ||
| roomShowMineQueryDto.endDate().toString(), | ||
| roomShowMineQueryDto.roomId().toString() | ||
| String.valueOf(dto.memberCount()), // 인원수 커서 | ||
| dto.roomId().toString() | ||
| )); | ||
| return nextCursor.toEncodedString(); |
There was a problem hiding this comment.
오호 함수형 인터페이스로 추출하셨네요 굿입니다!
| summary = "모집중인 방 검색", | ||
| description = "검색어(= 방이름 or 첵제목), 카테고리 와 매핑되는 모집중인 방을 검색합니다. 공개/비공개 방 모두 검색 가능합니다." | ||
| ) |
| List<RoomQueryDto> findRecruitingRoomsOrderByStartDateAsc(String keyword, LocalDate lastStartDate, Long roomId, int pageSize); | ||
| List<RoomQueryDto> findRecruitingRoomsWithCategoryOrderByStartDateAsc(String keyword, String categoryVal, LocalDate lastStartDate, Long roomId, int pageSize); | ||
| List<RoomQueryDto> findRecruitingRoomsOrderByMemberCountDesc(String keyword, Integer lastMemberCount, Long roomId, int pageSize); | ||
| List<RoomQueryDto> findRecruitingRoomsWithCategoryOrderByMemberCountDesc(String keyword, String categoryVal, Integer lastMemberCount, Long roomId, int pageSize); |
| /** | ||
| * 모집중인 방 검색 관련 메서드 | ||
| */ | ||
| @Override | ||
| public List<RoomQueryDto> findRecruitingRoomsOrderByStartDateAsc(String keyword, LocalDate lastStartDate, Long roomId, int pageSize) { | ||
| final LocalDate today = LocalDate.now(); | ||
| DateExpression<LocalDate> cursorExpr = room.startDate; // 커서 비교는 startDate | ||
|
|
||
| BooleanBuilder where = recruitingActiveWhere(today); | ||
| applyKeyword(where, keyword); | ||
| applyCursorStartDateAsc(where, cursorExpr, lastStartDate, roomId); | ||
|
|
||
| return queryFactory | ||
| .select(projectionForRecruitingRoomSearch()) | ||
| .from(room) | ||
| .join(room.bookJpaEntity, book) | ||
| .leftJoin(participant).on(participant.roomJpaEntity.eq(room)) | ||
| .where(where) | ||
| .groupBy( | ||
| room.roomId, | ||
| book.imageUrl, | ||
| room.title, | ||
| room.recruitCount, | ||
| room.startDate, | ||
| room.categoryJpaEntity.value | ||
| ) | ||
| .orderBy( | ||
| // 1차 정렬 : 설정된 정렬 조건, 2차 정렬 : 방이름으로 방 검색 > 책제목으로 방 검색 | ||
| toOrderSpecifier(pageable.getSort(), room, memberCountExpr), | ||
| priorityExpr.desc() | ||
| ) | ||
| .offset(pageable.getOffset()) | ||
| .limit(pageable.getPageSize()) | ||
| .orderBy(cursorExpr.asc(), room.roomId.asc()) | ||
| .limit(pageSize + 1) | ||
| .fetch(); | ||
| // TODO : 추후에 오프셋 페이징이 아니라, 키셋 페이징 기법 도입 검토 | ||
| } |
| /** 키워드(방 이름 OR 책 제목) 조건 추가 */ | ||
| private void applyKeyword(BooleanBuilder where, String keyword) { | ||
| // TODO : 현재는 like %keyword% 로 쿼리가 나감. 개선할 수 있는지 고민할 것 | ||
| if (keyword != null && !keyword.isBlank()) { | ||
| where.and(room.title.containsIgnoreCase(keyword).or(book.title.containsIgnoreCase(keyword))); | ||
| where.and(room.title.containsIgnoreCase(keyword) | ||
| .or(book.title.containsIgnoreCase(keyword))); | ||
| } | ||
| // category 필터 (빈 문자열이면 생략) | ||
| if (category != null && !category.isBlank()) { | ||
| where.and(room.categoryJpaEntity.value.eq(category)); | ||
| } |
There was a problem hiding this comment.
이것도 나중에 그냥 사용자 검색처럼 case 기반 가중치를 넣어서 정확도 올리는 정도로만 구현해도 충분할 것 같네여
There was a problem hiding this comment.
얍 일단 containsIgnoreCase 메서드로 구현해봤습니다
| public RoomSearchResponse searchRecruitingRooms(RoomSearchQuery query) { | ||
| // 1. validation | ||
| String sortVal = validateSort(sort); | ||
| String categoryVal = validateCategory(category); | ||
| RoomSearchSortParam sortParam = RoomSearchSortParam.from(query.sortStr()); | ||
| Category category = validateCategory(query.categoryStr()); | ||
|
|
||
| // 2. Pageable 생성 | ||
| int pageIndex = page > 0 ? page - 1 : 0; | ||
| Pageable pageable = PageRequest.of(pageIndex, DEFAULT_PAGE_SIZE, buildSort(sortVal)); | ||
| // 2. Cursor 생성 | ||
| Cursor cursor = Cursor.from(query.cursorStr(), DEFAULT_PAGE_SIZE); | ||
|
|
||
| // 3. 방 검색 | ||
| Page<RoomSearchResponse.RoomSearchResult> result = roomQueryPort.searchRoom(keyword, categoryVal, pageable); | ||
| CursorBasedList<RoomQueryDto> result = executeRecruitingRoomSearch(query, category, sortParam, cursor); | ||
|
|
||
| // TODO 검색 완료일 경우, 최근 검색어로 저장되도록 | ||
| recentSearchCreateManager.saveRecentSearchByUser(userId, keyword, RecentSearchType.ROOM_SEARCH, isFinalized); | ||
| // 4. 검색 완료일 경우, 최근 검색어 저장 | ||
| recentSearchCreateManager.saveRecentSearchByUser(query.userId(), query.keyword(), RecentSearchType.ROOM_SEARCH, query.isFinalized()); | ||
|
|
||
| // 4. response 구성 | ||
| // 5. response 구성 | ||
| return new RoomSearchResponse( | ||
| result.getContent(), | ||
| page, | ||
| result.getNumberOfElements(), | ||
| result.isLast(), | ||
| result.isFirst()); | ||
| roomQueryMapper.toRoomSearchResponse(result.contents()), | ||
| result.nextCursor(), | ||
| result.isLast() | ||
| ); | ||
| } |
#️⃣ 연관된 이슈
📝 작업 내용
방 검색 api를 수정하였습니다
수정 사항 정리
커서 기반 페이지네이션으로 수정
회의 결과 방 검색 api의 request param 을
로 확정하였습니다
이에 맞게 전반적인 코드 및 테스트 코드를 수정, 추가 하였습니다
현재는 QueryDSL 코드가 키워드를 포함하는 방 이름 or 책 제목을 select 합니다
일단은 위와 같이 구현하였는데, 추후 성능적으로 개선할 방법이 있는지 고민해볼 포인트가 될 수 있을 것 같습니다!!
해당 부분은 TODO 주석처리 해두었습니다!
📸 스크린샷
💬 리뷰 요구사항
📌 PR 진행 시 이러한 점들을 참고해 주세요
Summary by CodeRabbit