Skip to content

[Refactor] ver 1.3.0 모임방 사용성 개선 작업#304

Merged
hd0rable merged 12 commits intodevelopfrom
refactor/#302-room-improve
Sep 16, 2025
Merged

[Refactor] ver 1.3.0 모임방 사용성 개선 작업#304
hd0rable merged 12 commits intodevelopfrom
refactor/#302-room-improve

Conversation

@hd0rable
Copy link
Member

@hd0rable hd0rable commented Sep 14, 2025

#️⃣ 연관된 이슈

closes #302

📝 작업 내용

  • 버전 1.3.0의 바뀐요구사항에 따라 관련 리펙토링 작업을 수행했습니다.
  • 이슈에 자세히 작성해두었으니 확인해주시면 감사하겠습니다.
  • 관련 노션 링크
  • 토큰 유효기간 환경변수 추출로 인해 이 pr이 머지되기전에 dev.yml / 배포되기전에 prod.yml 업데이트하겠습니다!!

📸 스크린샷

💬 리뷰 요구사항

  • 방 검색 부분에 전체 검색플래그가 추가되면서 로직이 헷갈리는감이있는데.. 제대로 구현된것이 맞는지 확인해주시면 감사하겠슴다

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

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

Summary by CodeRabbit

  • New Features

    • 홈에 마감/인기/최신(RoomGetDeadlinePopularRecent) 방 목록 추가.
    • /rooms/{roomId}로 진행중/만료 통합 상세 조회 도입.
    • 검색에 전체카테고리 옵션(isAllCategory) 및 모드 기반 검색(RoomSearchMode) 추가.
    • JWT 만료시간을 설정으로 외부화.
  • Documentation

    • Swagger 응답명·스키마 정리 및 응답 필드 설명 추가(deadlineDate 등).
  • Tests

    • 다수 통합 테스트를 클래스 단위 @transactional로 전환하여 자동 롤백 적용.
  • Chores / Refactor

    • 응답 DTO명·필드 및 관련 서비스/포트 응답 타입 정비.
    • 최근검색 저장 시 키워드 정규화 및 처리 개선.

@coderabbitai
Copy link

coderabbitai bot commented Sep 14, 2025

Walkthrough

JWT 만료 시간을 프로퍼티로 주입하도록 변경. 진행중/만료 통합 상세 조회 엔드포인트로 합치고 deadline/popular에 recent 목록 추가. 검색에 isAllCategory 파라미터·모드·검증 추가. 홈 참여 매핑·쿼리·DTO 확장. 다수 테스트를 @transactional로 전환 및 필요시 flush/clear 적용. (50단어 이내)

Changes

Cohort / File(s) Summary
Security: JWT 만료 주입
src/main/java/.../common/security/util/JwtUtil.java
하드코딩된 만료 상수 제거, @Valuejwt.access-token-expirationjwt.signup-token-expiration 주입.
Swagger 응답명 변경
src/main/java/.../common/swagger/SwaggerResponseDescription.java
enum 상수명 변경: ROOM_PLAYING_DETAILROOM_PLAYING_OR_EXPIRED_DETAIL, ROOM_GET_DEADLINE_POPULARROOM_GET_DEADLINE_POPULAR_RECENT.
유틸: 날짜 포맷 변경/추가
src/main/java/.../common/util/DateUtil.java
메서드명 케이스 변경(RecruitingRoomFormatAfterTimerecruitingRoomFormatAfterTime) 및 recruitingRoomFormatAfterTimeSimple 추가.
컨트롤러 / 응답 DTO 변경
src/main/java/.../room/adapter/in/web/RoomQueryController.java, .../response/RoomPlayingOrExpiredDetailViewResponse.java, .../response/RoomGetDeadlinePopularRecentResponse.java
/rooms/{roomId} 통합 상세 엔드포인트 및 응답 타입 변경; deadline/popular에 recent 목록 추가를 위한 DTO 도입; 관련 예외 설명 상수 교체.
검색 파라미터 · 쿼리 DTO
src/main/java/.../room/application/port/in/dto/RoomSearchQuery.java, .../dto/RoomSearchMode.java
RoomSearchQueryisAllCategory 필드 추가 및 factory 확장; RoomSearchMode enum 추가로 모드 결정·키워드 정규화·검증 도입.
서비스·매퍼 변경
src/main/java/.../room/application/service/*.java, .../application/mapper/RoomQueryMapper.java, .../application/mapper/RoomParticipantQueryMapper.java
검색 파라미터 검증·모드 기반 실행 도입, deadline/popular/recent 조합 반환 로직 추가, 홈 참여 매핑을 상태 기반 계산으로 변경(MapStruct→default 구현).
포트·퍼시스턴스·레포지토리 개편
src/main/java/.../room/application/port/out/RoomQueryPort.java, .../adapter/out/persistence/RoomQueryPersistenceAdapter.java, .../repository/...RoomQueryRepository*.java
카테고리별 deadline/popular에서 userId 제거, recent(생성순) 조회 메서드 추가, 쿼리 상태·조건 정리 및 정렬 로직 조정.
홈 참여 DTO 강화
src/main/java/.../room/adapter/in/web/response/RoomGetHomeJoinedListResponse.java, .../application/port/out/dto/RoomParticipantQueryDto.java
JoinedRoomInfo에 deadlineDate@Schema 주석 추가; RoomParticipantQueryDtoRoomStatus 필드 추가 및 null 검증.
투표 상위 항목 타입 변경
src/main/java/.../roompost/adapter/out/persistence/VoteQueryPersistenceAdapter.java, .../repository/vote/VoteQueryRepository*.java, .../application/port/out/VoteQueryPort.java
투표 반환 타입을 RoomPlayingOrExpiredDetailViewResponse.CurrentVote로 변경 및 매핑 갱신.
블라인드 문자열 생성 로직 변경
src/main/java/.../roompost/application/service/validator/RoomPostAccessValidator.java
블러 처리 알고리즘 변경: 공백·특수문자 보존, 영숫자 문자별 순환 치환 방식으로 내부 구현 변경.
테스트 전반 트랜잭션화
src/test/java/... (다수, 예: .../room/.../RoomPlayingOrExpiredDetailViewApiTest.java, .../room/.../RoomGetDeadlinePopularRecentApiTest.java, .../room/.../RoomSearchApiTest.java, .../user/..., .../feed/..., .../book/...)
다수 테스트 클래스에 @Transactional 추가, @AfterEach 수동 정리 제거, 필요시 EntityManager.flush()/clear() 삽입 및 일부 테스트 기대치·내용 조정.
에러 메시지 변경
src/main/java/.../common/exception/code/ErrorCode.java
ROOM_IS_EXPIRED 메시지 문구 변경.
타입/인터페이스 재배치
src/main/java/.../room/application/port/in/*.java
기존 RoomGetDeadlinePopularUseCase, RoomShowPlayingDetailViewUseCase 제거 및 RoomGetDeadlinePopularRecentUseCase, RoomShowPlayingOrExpiredDetailViewUseCase 추가.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as 사용자
  participant C as RoomQueryController
  participant S as RoomShowPlayingOrExpiredDetailViewService
  participant P as RoomQueryPort/Repository

  U->>C: GET /rooms/{roomId}
  C->>S: getPlayingOrExpiredRoomDetailView(userId, roomId)
  S->>P: 조회: Room, RoomParticipant(사용자), Votes
  P-->>S: 엔티티/쿼리결과
  S-->>C: RoomPlayingOrExpiredDetailViewResponse
  C-->>U: 200 OK
Loading
sequenceDiagram
  autonumber
  actor U as 사용자
  participant C as RoomQueryController
  participant S as RoomGetDeadlinePopularRecentService
  participant P as RoomQueryPort

  U->>C: GET /rooms/home?category=...
  C->>S: getDeadlineAndPopularAndRecentRoomList(category)
  S->>P: findRoomsByCategoryOrderByStartDateAsc(...)
  P-->>S: deadline list
  S->>P: findRoomsByCategoryOrderByMemberCount(...)
  P-->>S: popular list
  S->>P: findRoomsByCategoryOrderByCreatedAtDesc(now - 72h, ...)
  P-->>S: recent list
  S-->>C: RoomGetDeadlinePopularRecentResponse
  C-->>U: 200 OK
Loading
sequenceDiagram
  autonumber
  actor U as 사용자
  participant C as RoomQueryController
  participant S as RoomSearchService
  participant Q as RoomQueryPort/Repository

  U->>C: GET /rooms/search?isAllCategory=...
  C->>S: searchRecruitingRooms(query{isAllCategory,...})
  S->>S: validateSearchParams()
  alt invalid params
    S-->>C: throw API_INVALID_PARAM
    C-->>U: 400 Bad Request
  else valid
    S->>Q: execute search with effectiveKeyword/category/mode
    Q-->>S: results
    S-->>C: RoomSearchResponse
    C-->>U: 200 OK
  end
Loading

Estimated code review effort

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

Possibly related issues

Possibly related PRs

Poem

(_/)
(•ㅅ•) 깡총, 토끼의 축하:
"토큰은 설정으로, 방 상세는 하나로 합쳤지요.
마감·인기·최근, 한 접시에 담았어요.
검색은 더 똑똑해지고, 테스트는 트랜잭션으로 안전해요." 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Linked Issues Check ❓ Inconclusive 제공된 변경 요약을 기준으로 PR은 대체로 [#302] 체크리스트 항목들을 반영하고 있습니다: 진행중/만료 통합 상세조회(컨트롤러·서비스·포트·DTO 변경), JWT 만료시간을 @value로 분리, 최근 생성(72시간) 스트림 추가, isAllCategory 파라미터·RoomSearchMode 기반 검증 도입, 블라인드 치환 로직 변경, 그리고 다수의 테스트에 @transactional 적용이 반영되어 있습니다. 다만 요약만으로는 저장소/환경파일(dev/prod)에서의 최종 jwt 값(특히 prod=1일) 반영 여부, 리포지터리/매퍼에서 '모임홈의 최대 1일' 표시가 정확히 컷오프되는지, 그리고 모든 테스트 파일이 빠짐없이 @transactional로 전환되었는지를 확증할 수 없습니다. 해결: 병합 전 CI를 통해 전체 테스트를 실행하여 누락된 테스트가 없는지 확인하고 dev/prod 설정에 jwt 만료값(개발/테스트=30일, 운영=1일)이 반영되었는지 검증하세요; 또한 모임홈의 '최대 1일' 컷오프와 블라인드 치환의 공백·특수문자 보존 동작을 단위 또는 통합 테스트로 명시적으로 확인해 주세요.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed PR 제목 "[Refactor] ver 1.3.0 모임방 사용성 개선 작업"은 변경의 핵심인 '모임방 사용성 개선'과 리팩토링 의도를 간결하게 요약하고 있으며 불필요한 파일 목록이나 이모지 없이 읽기 쉽습니다.
Out of Scope Changes Check ✅ Passed 검토된 변경 항목들 대부분이 #302 요구사항(상세조회 통합, 테스트 트랜잭셔널 전환, jwt 환경 분리, 최근 방 추가, 검색 모드 도입, 블라인드 치환 등)과 직접 관련되어 있으므로 명백한 범위 벗어난 변경은 발견되지 않았습니다; Swagger 열거형명 변경 및 ErrorCode 메시지 수정은 컨트롤러/예외명 변경에 맞춘 일관성 작업으로 보입니다.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ 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/#302-room-improve

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


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

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

github-actions bot commented Sep 14, 2025

Test Results

470 tests   470 ✅  43s ⏱️
139 suites    0 💤
139 files      0 ❌

Results for commit 0597611.

♻️ 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: 17

Caution

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

⚠️ Outside diff range comments (10)
src/test/java/konkuk/thip/roompost/adapter/in/web/RoomPostSearchApiTest.java (1)

343-351: 하드코딩된 블러 문자열 의존 제거 — 속성 기반 검증으로 전환 제안

구현 세부(블러 문자열 상수/알고리즘)에 강하게 결합되어 테스트가 쉽게 깨집니다. PR 목표(글자수·공백 동일, 특수문자 유지)에 맞춰 잠김/비잠김 케이스를 원문 대비 속성 기반으로 검증하는 편이 견고합니다.

아래처럼 수정하면:

  • 잠긴 글: 원문과 달라야 하고, 길이와 공백 수는 동일해야 함
  • 잠기지 않은 글: 원문과 동일해야 함
  • 잠김/비잠김 포스트 모두 응답에 존재하는지 보장
-        for (JsonNode post : postList) {
-            boolean isLocked = post.path("isLocked").asBoolean();
-            String content = post.path("content").asText();
-
-            if (isLocked) {
-                assertThat(content).contains("태정 태세문단세");
-            }
-        }
+        String lockedOriginal = "잠긴 내용입니다";
+        String unlockedOriginal = "잠기지 않은 내용입니다";
+        boolean seenLocked = false;
+        boolean seenUnlocked = false;
+        for (JsonNode post : postList) {
+            int page = post.path("page").asInt();
+            boolean isLocked = post.path("isLocked").asBoolean();
+            String content = post.path("content").asText();
+            if (page == 50) { // 잠긴 기록
+                assertThat(isLocked).isTrue();
+                assertThat(content).isNotEqualTo(lockedOriginal);
+                assertThat(content.length()).isEqualTo(lockedOriginal.length());
+                long spaceCount = content.chars().filter(ch -> ch == ' ').count();
+                long originalSpaceCount = lockedOriginal.chars().filter(ch -> ch == ' ').count();
+                assertThat(spaceCount).isEqualTo(originalSpaceCount);
+                seenLocked = true;
+            } else if (page == 5) { // 잠기지 않은 기록
+                assertThat(isLocked).isFalse();
+                assertThat(content).isEqualTo(unlockedOriginal);
+                seenUnlocked = true;
+            }
+        }
+        assertThat(seenLocked).isTrue();
+        assertThat(seenUnlocked).isTrue();
src/main/java/konkuk/thip/common/security/util/JwtUtil.java (1)

73-75: isExpired 시그니처/예외 처리 정교화(boolean 반환 + 예외 안전)

현재 Boolean 반환이며 파싱 실패 시 예외가 그대로 전파됩니다. 호출부 혼란을 줄이려면 boolean으로 단순화하고 파싱 예외는 만료(true)로 취급하거나 별도 메서드로 분리하세요.

적용 예시(diff):

-    public Boolean isExpired(String token) {
-        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
-    }
+    public boolean isExpired(String token) {
+        try {
+            Date exp = Jwts.parser().verifyWith(secretKey).build()
+                    .parseSignedClaims(token).getPayload().getExpiration();
+            return exp.before(new Date());
+        } catch (ExpiredJwtException e) {
+            return true;
+        } catch (JwtException | IllegalArgumentException e) {
+            // 파싱 실패 등: 유효하지 않은 토큰으로 간주
+            log.debug("Failed to parse JWT while checking expiration", e);
+            return true;
+        }
+    }
src/main/java/konkuk/thip/roompost/application/service/validator/RoomPostAccessValidator.java (2)

42-46: null 허용 파라미터 검증 로직 오류 — 오토언박싱 NPE 및 요구사항 불일치

“모든 파라미터는 null이어야 함” 요구에 비추어 isPageFilter, isOverview는 값이 아닌 “존재 여부(null 아님)”를 확인해야 합니다. 현재는 불리언 평가로 NPE 위험 + false 전달 시도 허용되는 버그가 있습니다.

-        if (pageStart != null || pageEnd != null || isPageFilter || isOverview || sort != null) {
+        if (pageStart != null || pageEnd != null || isPageFilter != null || isOverview != null || sort != null) {
             throw new BusinessException(ErrorCode.API_INVALID_PARAM, new IllegalArgumentException("내 기록 조회에서는 roomId, type, cursor를 제외한 모든 파라미터는 null이어야 합니다."));
         }

12-40: Boolean 래퍼(null 가능) 직접 연산으로 인한 NPE 위험 — 즉시 수정 필요

validateGroupRoomPostFilters(...)와 validateMyRoomPostFilters(...)에서 Boolean(isPageFilter, isOverview)를 !, &&, || 등으로 직접 사용하고 있어 null 전달 시 오토언박싱 NPE가 발생합니다.
위치: src/main/java/konkuk/thip/roompost/application/service/validator/RoomPostAccessValidator.java (메서드 validateGroupRoomPostFilters, validateMyRoomPostFilters). 호출부: src/main/java/konkuk/thip/roompost/application/service/RoomPostSearchService.java:74.

해결 방안(간단): 메서드 시작부에서 안전 변환 또는 시그니처를 primitive boolean으로 변경하세요. 예:
final boolean pageFilter = Boolean.TRUE.equals(isPageFilter);
final boolean overview = Boolean.TRUE.equals(isOverview);
이후 모든 조건문에서 pageFilter / overview 를 사용하도록 교체.

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

189-195: HOST 전환은 삭제/재삽입 대신 필드 업데이트로 단순화

불필요한 delete → insert 대신 롤만 변경하면 됩니다. flush까지 해 두면 이후 조회 일관성이 보장됩니다.

-        RoomParticipantJpaEntity firstMember = roomParticipantJpaRepository.findAllByRoomId(targetRoom.getRoomId()).get(1);
-        roomParticipantJpaRepository.delete(firstMember);
-        RoomParticipantJpaEntity roomCreator = roomParticipantJpaRepository.save(RoomParticipantJpaEntity.builder()
-                .userJpaEntity(firstMember.getUserJpaEntity())
-                .roomJpaEntity(firstMember.getRoomJpaEntity())
-                .roomParticipantRole(RoomParticipantRole.HOST)
-                .build());      // firstMember 을 MEMBER -> HOST 로 수정
+        RoomParticipantJpaEntity firstMember = roomParticipantJpaRepository.findAllByRoomId(targetRoom.getRoomId()).get(1);
+        firstMember.setRoomParticipantRole(RoomParticipantRole.HOST);
+        RoomParticipantJpaEntity roomCreator = roomParticipantJpaRepository.save(firstMember);
+        roomParticipantJpaRepository.flush();

106-132: 참여자 선택의 순서 의존성 제거: 저장 결과를 반환해 직접 선택

findAllByRoomId(...).get(1)은 정렬 보장이 없다면 플래키할 수 있습니다. 사용자 생성/매핑 헬퍼가 저장된 유저 리스트를 반환하도록 바꾸고 그 중 하나를 선택하세요.

-    private void saveUsersToRoom(RoomJpaEntity roomJpaEntity, int count) {
+    private List<UserJpaEntity> saveUsersToRoom(RoomJpaEntity roomJpaEntity, int count) {
@@
-        roomParticipantJpaRepository.saveAll(mappings);
-    }
+        roomParticipantJpaRepository.saveAll(mappings);
+        return savedUsers;
+    }

테스트 1:

-        saveUsersToRoom(targetRoom, 4);
-        UserJpaEntity joiningUser = roomParticipantJpaRepository.findAllByRoomId(targetRoom.getRoomId()).get(1).getUserJpaEntity();
+        List<UserJpaEntity> createdUsers = saveUsersToRoom(targetRoom, 4);
+        UserJpaEntity joiningUser = createdUsers.get(1);

테스트 3:

-        saveUsersToRoom(targetRoom, 4);
-        UserJpaEntity joiningUser = roomParticipantJpaRepository.findAllByRoomId(targetRoom.getRoomId()).get(1).getUserJpaEntity();
+        List<UserJpaEntity> createdUsers = saveUsersToRoom(targetRoom, 4);
+        UserJpaEntity joiningUser = createdUsers.get(1);

테스트 2는 호출부 변경 없이 그대로 둬도 됩니다.

Also applies to: 139-141, 241-245, 292-294

src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteQueryRepositoryImpl.java (1)

56-89: N+1(투표 항목) 제거 및 정렬 안정성 개선

  • 현재 상위 투표 N개 조회 후 각 투표별로 항목을 추가 조회합니다. 벌크 조회 후 groupingBy로 메모리 매핑하면 왕복을 1회로 줄일 수 있습니다.
  • 총합 동률에 대한 타이브레이커(예: vote.id.desc())를 추가해 결정성을 높이세요.

리팩터링 방향(개요):

  • 1단계: 상위 topVotes의 ID 수집
  • 2단계: voteItemwhere vote_id in (:ids)로 일괄 조회, orderBy count desc, voteItemId asc
  • 3단계: Map<voteId, List<VoteItem>>로 그룹핑 후 DTO 구성
src/test/java/konkuk/thip/room/adapter/in/web/RoomGetDeadlinePopularRecentApiTest.java (1)

101-121: recent 정렬 단언 플래키 가능성 — createdAt 동률 타이브레이커 또는 단언 완화 필요

createdAt이 동일(초 단위)하면 정렬이 비결정적일 수 있습니다.
대안:

  • 리포지토리 정렬에 createdAt desc, roomId desc 추가(권장), 또는
  • 테스트 단언을 순서 고정 대신 집합/부분 순서 검증으로 완화.

또한 최근 목록에서도 비공개 방 제외를 함께 검증하면 좋습니다. 그리고 110/117의 deadlineDate 단언은 중복입니다.

테스트 완화 예시(diff):

-                .andExpect(jsonPath("$.data.recentRoomList[0].roomId").value(rooms.get(9).getRoomId()))
-                .andExpect(jsonPath("$.data.recentRoomList[1].roomId").value(rooms.get(8).getRoomId()));
+                .andExpect(jsonPath("$.data.recentRoomList[*].roomId",
+                    containsInAnyOrder(
+                        rooms.get(9).getRoomId().intValue(),
+                        rooms.get(8).getRoomId().intValue(),
+                        rooms.get(7).getRoomId().intValue(),
+                        rooms.get(6).getRoomId().intValue()
+                    )))
+                .andExpect(jsonPath("$.data.recentRoomList[*].roomId",
+                    not(hasItem(privateRoom.getRoomId().intValue()))));
src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingOrExpiredDetailViewApiTest.java (1)

60-86: 만료 방(EXPIRED) 픽스처의 날짜/상태 불일치

현재 saveScienceRoom은 endDate를 항상 오늘+30일로 고정합니다. EXPIRED 상태와 모순이라 도메인 일관성이 깨집니다(스케줄러 규칙상 EXPIRED는 end_date < today). 테스트 신뢰도를 위해 상태에 따라 endDate를 분기해 주세요.

다음과 같이 조정 제안:

-                .endDate(LocalDate.now().plusDays(30))
+                .endDate(EXPIRED.equals(roomStatus) ? LocalDate.now().minusDays(1) : LocalDate.now().plusDays(30))
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java (1)

205-254: [홈 참여방] 정렬 기준과 커서(키셋) 불일치 — priority(진행/모집) 키 포함 필수

  • 검증 결과: Repository 구현은 현재 participant.userPercentage 기반 CASE 정렬(progress desc) + startDate, roomId만 사용하고 있습니다 (src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java). 호출부(RoomQueryPersistenceAdapter)는 lastUserPercentage, lastStartDate, lastRoomId만 전달하고 있어 priority/우선순위 정보가 커서에 포함되어 있지 않습니다 (src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:78). 컨트롤러는 단일 cursor 문자열만 수신합니다 (src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java). → 페이지 전개 중 항목 누락/순서 뒤섞임 위험이 확실함.

  • 조치(간결): 정렬과 커서 키셋을 동일하게 통일하세요.

    • 정렬 우선순위: priority(0=IN_PROGRESS,1=RECRUITING) asc → orderKey(desc: IN_PROGRESS→participant.userPercentage, RECRUITING→0.0) desc → room.startDate.asc() → room.roomId.asc()
    • 커서에 priorityCursor, progressCursor(orderKey), startDateCursor, roomIdCursor 포함하도록 repository 메서드 시그니처 및 쿼리(where/orderBy) 수정.
    • persistence adapter 호출부, 서비스/컨트롤러의 cursor 인코딩·디코딩(nextCursor 생성/파싱) 모두 함께 변경.
  • 수정 대상(우선 확인 및 변경 필요 파일):

    • src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java
    • src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepository.java
    • src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java
    • src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java
    • src/main/java/konkuk/thip/room/application/service/RoomGetHomeJoinedListService.java
♻️ Duplicate comments (1)
src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteQueryRepositoryImpl.java (1)

6-6: 리포지토리가 웹 응답 타입에 직접 의존 — 계층 위반(중복 지적)

RoomPlayingOrExpiredDetailViewResponse는 웹 어댑터 표현입니다. 리포지토리는 쿼리 DTO를 반환하고 상위 계층에서 웹 DTO로 변환하도록 분리해 주세요.

🧹 Nitpick comments (56)
src/test/java/konkuk/thip/user/adapter/in/web/UserIsFollowingApiTest.java (1)

25-25: 클래스 레벨 @transactional 적용 시 커밋 의존 로직 주의

MockMvc 호출과 리포지토리 작업이 동일 트랜잭션으로 롤백되므로 대부분의 경우 문제없습니다. 다만 아래 케이스가 있다면 실제 운영 커밋 시맨틱과 다르게 동작할 수 있습니다:

  • 서비스 메서드에 REQUIRES_NEW 사용
  • @TransactionalEventListener(phase = AFTER_COMMIT) 등 커밋 이후 이벤트 의존
  • DB 트리거/제약조건 검증을 반드시 커밋 시점에 기대

본 테스트는 단순 생성 후 조회라 이슈 가능성은 낮지만, 안전하게 하려면 save 직후 한 번 flush()를 권장합니다(가시성/트리거 보장).

예시(참고용, 테스트 본문에 추가):

followingJpaRepository.flush();

필요 시, 커밋 의존 케이스는 해당 테스트만 @Commit 또는 트랜잭션 비활성(@Transactional(propagation = Propagation.NOT_SUPPORTED))로 분리 검토 바랍니다.

src/main/java/konkuk/thip/common/util/DateUtil.java (4)

90-92: 1분 미만 구간에서 '0분' 노출 가능성

Duration.toMinutes()는 버림이므로 59초 등에서 "0분"이 노출됩니다. 최소 1분 보정(올림) 권장.

다음과 같이 올림 처리로 수정해 주세요:

-        long minutes = d.toMinutes();
-        return minutes + "분";
+        long minutes = (d.getSeconds() + 59) / 60; // 1분 미만 올림
+        return minutes + "분";

80-84: 홈 요구사항(“최대 1일까지만”)과의 정합성 확인 필요

현재 days>0이면 그대로 "N일"을 반환합니다. 모임 홈에서 “최대 1일까지만”을 노출해야 한다면 상위 레이어에서 캡핑하든지, 본 유틸에서 cap 옵션을 제공해야 합니다. 사용처에서 이미 캡핑 중인지 확인 부탁드립니다.


72-75: 테스트 용이성과 타임존 안정성 향상 제안

LocalDateTime.now() 직접 호출은 테스트가 까다롭고 서버 TZ 영향을 받습니다. Clock 주입(혹은 오버로드)로 고정 now를 허용하면 단위 테스트 신뢰도가 올라갑니다.


71-92: 중복 로직 경량 공통화 제안

본 메서드와 RecruitingRoomFormatAfterTime가 거의 동일한 전처리를 공유합니다. Duration 계산/가드만이라도 private 헬퍼로 공통화하면 드리프트를 줄일 수 있습니다.

src/test/java/konkuk/thip/roompost/application/service/VoteCreateServiceTest.java (6)

84-88: 검증 누락: currentPage도 함께 단언 필요

테스트 설명에 currentPage 변경이 포함되어 있으나 단언이 없습니다. 한 줄 추가로 의도와 일치시켜 주세요.

-        // userPercentage, roomPercentage 값 update 확인
+        // currentPage, userPercentage, roomPercentage 값 update 확인
+        assertThat(roomParticipant.getCurrentPage()).isEqualTo(89);
         // 허용 오차범위를 10의 -6제곱(= 0.000001) 로 설정

82-82: 특정 엔티티 재조회는 ID 기반으로

findAll().get(0)은 암묵적 순서에 의존합니다. 생성한 room의 ID로 재조회하세요.

-        RoomJpaEntity refreshRoom = roomJpaRepository.findAll().get(0);
+        RoomJpaEntity refreshRoom = roomJpaRepository.findById(room.getRoomId()).orElseThrow();

32-32: MockMvc 미사용 시 애노테이션 제거로 컨텍스트 경량화

이 테스트에서는 MockMvc를 사용하지 않습니다. 불필요한 컨텍스트 부팅을 줄일 수 있습니다.

-@AutoConfigureMockMvc(addFilters = false)

53-53: 불필요한 throws Exception 제거

테스트 본문에서 예외를 던지지 않습니다. 시그니처 단순화하세요.

-    void vote_create_room_participant_and_room_percentage_update() throws Exception {
+    void vote_create_room_participant_and_room_percentage_update() {

77-77: 주석 영역 오타(naming) 정리

when/then 구간 주석이 뒤바뀌었습니다. 가독성 위해 정리 추천.

-        //then
+        // when
 ...
-        //then
+        // then

Also applies to: 80-80


77-83: createVote() 직후 DB 반영 및 1차 캐시 무효화(em.flush()/em.clear() 추가) 권장

서비스가 별도 트랜잭션/영속성 컨텍스트(REQUIRES_NEW 등)를 쓰면 테스트의 1차 캐시로 변경이 보이지 않을 수 있으므로 createVote() 직후 1차 캐시를 비우세요.

적용 제안(diff):

         voteCreateService.createVote(command);
+        // DB 반영 및 1차 캐시 초기화
+        em.flush();
+        em.clear();

필요한 임포트/주입:

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;

@PersistenceContext
private EntityManager em;
src/main/java/konkuk/thip/common/security/util/JwtUtil.java (4)

3-8: 와일드카드 import 제거 및 필요한 타입만 명시

import io.jsonwebtoken.*;는 다른 명시적 import와 중복이며, Claims/Jws만 필요합니다. 와일드카드 제거로 의존 타입을 명확히 하세요.

적용 예시(diff):

@@
-import io.jsonwebtoken.ExpiredJwtException;
-import io.jsonwebtoken.Jwts;
-import io.jsonwebtoken.MalformedJwtException;
-import io.jsonwebtoken.UnsupportedJwtException;
-import io.jsonwebtoken.security.SignatureException;
-import io.jsonwebtoken.*;
+import io.jsonwebtoken.ExpiredJwtException;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.MalformedJwtException;
+import io.jsonwebtoken.UnsupportedJwtException;
+import io.jsonwebtoken.security.SignatureException;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jws;
+import io.jsonwebtoken.JwtException;

59-68: JWT 검증 로그 레벨/내용 조정(정보 노출 최소화)

서명/형식 오류를 INFO+스택트레이스로 남기면 운영 로그가 과도해지고 토큰 단서가 노출될 수 있습니다. 메시지만 WARN(또는 DEBUG에 스택)으로 조정 권장.

적용 예시(diff):

-        } catch (SignatureException e) {
-            log.info("Invalid JWT Signature", e);
-        } catch (MalformedJwtException e) {
-            log.info("Invalid JWT Token", e);
-        } catch (ExpiredJwtException e) {
-            log.info("Expired JWT Token", e);
-        } catch (UnsupportedJwtException e) {
-            log.info("Unsupported JWT Token", e);
-        } catch (IllegalArgumentException e) {
-            log.info("JWT claims string is empty.", e);
+        } catch (SignatureException e) {
+            log.warn("Invalid JWT signature");
+        } catch (MalformedJwtException e) {
+            log.warn("Invalid JWT token");
+        } catch (ExpiredJwtException e) {
+            log.warn("Expired JWT token");
+        } catch (UnsupportedJwtException e) {
+            log.warn("Unsupported JWT token");
+        } catch (IllegalArgumentException e) {
+            log.warn("JWT claims string is empty");

77-83: 중복 파싱 제거로 비용/예외 포인트 축소

getOauth2Id/getUserId/getLoginUser에서 토큰을 2회 파싱합니다. 한 번 파싱한 Claims를 재사용하도록 리팩터링하면 성능/안정성이 좋아집니다.

예: parseClaims(token) 헬퍼를 추가하고 반환된 Claims로 두 값을 동시에 조회.

Also applies to: 85-93


28-31: 주입 스타일 일관성

현재 secret은 생성자 주입, 만료값은 필드 주입입니다. 테스트 용이성과 불변성 확보 차원에서 전부 생성자 주입으로 통일하는 것을 권장합니다(선택 사항).

원하시면 @ConfigurationProperties(prefix = "jwt") + @Validated로 묶어 바인딩/검증까지 일원화하는 예시도 드릴 수 있습니다.

src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckDeleteApiTest.java (1)

76-77: flush + clear 위치 적절

컨트롤러 처리 직후 flush/clear로 1차 캐시를 비우고 DB 상태를 재조회하는 패턴이 타당합니다. 동일 패턴이 반복된다면 헬퍼 메서드로 추출을 고려해도 좋습니다.

src/main/java/konkuk/thip/roompost/application/service/validator/RoomPostAccessValidator.java (3)

58-71: 블라인드 규칙 충족(길이/공백/특수문자 유지) — 조건 단순화 제안

현재 구현은 요구사항(글자수 동일, 공백/특수문자 보존)을 충족합니다. 조건을 약간 단순화하면 가독성이 좋아집니다.

-            // 특수문자/공백일 경우 그대로 append
-            if (Character.isWhitespace(ch) || isSpecialCharacter(ch)) {
+            // 특수문자/공백(= 비 영숫자)일 경우 그대로 append
+            if (isSpecialCharacter(ch)) {
                 sb.append(ch);
             } else {
                 // 나머지 문자들은 모두 치환
                 sb.append(BLURRED_STRING.charAt(blurIndex));
                 blurIndex = (blurIndex + 1) % blurLen; // 순환
             }

추가(옵션): 유니코드 보편성 강화를 위해 code point 단위 순회로의 전환도 고려 가능합니다.


76-79: 주석 의미 불일치 — 유니코드 기준으로 정정

구현은 “비 영숫자(Unicode)”를 특수문자로 간주합니다. 주석의 “아스키” 표현을 실제 동작과 일치시키세요.

-        // 아스키 문자 중 문자/숫자만 제외하고 모두 특수문자 처리 예시
+        // 유니코드 기준: 문자/숫자를 제외한 모든 문자를 특수문자로 간주
         return !Character.isLetterOrDigit(ch);

50-74: 블라인드 로직 전반 LGTM — 테스트 커버리지 보강 제안

동작은 깔끔합니다. 회귀 방지를 위해 아래 케이스 중심의 단위 테스트 추가를 권합니다:

  • 공백/탭/개행 유지, 이모지/이모티콘 유지, 한글/영문/숫자 치환, 혼합 문자열 길이 동일성, 매우 긴 문자열 순환 치환.

원하시면 테스트 스켈레톤을 생성해 드립니다.

src/test/java/konkuk/thip/room/adapter/in/web/RoomRecruitingDetailViewApiTest.java (4)

39-39: 클래스 레벨 @transactional 유지 OK · 필요 시 테스트 메서드 단위로 경계 분리 고려

특정 케이스에서 “요청 트랜잭션과 테스트 트랜잭션 분리”가 필요하면 해당 테스트 메서드에 한해 @Transactional(propagation = NOT_SUPPORTED) 또는 TestTransaction.end()/start()로 경계를 분리해 주세요. 이벤트(AFTER_COMMIT) 검증 등에서 유용합니다.


137-171: 시간 의존성 완화: 기준 날짜 고정

LocalDate.now()를 다중 호출하면 드문 타이밍 이슈가 생길 수 있습니다. 메서드 내 기준 날짜를 고정해 사용하세요.

-        //given
-        RoomJpaEntity targetRoom = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1), 10, RoomStatus.RECRUITING);
+        //given
+        LocalDate today = LocalDate.now();
+        RoomJpaEntity targetRoom = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", today.plusDays(1), 10, RoomStatus.RECRUITING);
@@
-                .andExpect(jsonPath("$.data.progressStartDate", is(DateUtil.formatDate(LocalDate.now().plusDays(1)))))
+                .andExpect(jsonPath("$.data.progressStartDate", is(DateUtil.formatDate(today.plusDays(1)))))

(동일 패턴을 본 파일의 다른 테스트에도 적용 권장)


315-315: 가독성 닛픽: 빈 배열 검증은 empty() 매처 사용 가능

hasSize(0) 대신 empty()가 의도를 더 잘 드러냅니다.

-                .andExpect(jsonPath("$.data.recommendRooms", hasSize(0)));
+                .andExpect(jsonPath("$.data.recommendRooms", empty()));

110-116: oauth2Id 중복 방지 — 테스트 데이터에 시퀀스 추가

UserJpaEntity의 oauth2_id에 DB 레벨 unique 제약이 없습니다 (src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java:38). 테스트에서 동일한 oauth2Id를 반복 사용하면 혼동/충돌 소지가 있으므로 각 사용자에 시퀀스(i)를 붙여 고유화하세요.
수정 위치: src/test/java/konkuk/thip/room/adapter/in/web/RoomRecruitingDetailViewApiTest.java (라인 110-116)

-                        .oauth2Id("oauth2Id")
+                        .oauth2Id("oauth2Id-" + i)
src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllApiTest.java (1)

40-40: 클래스 레벨 @transactional 전환은 👍 — 다만 JDBC로 타임스탬프 덮어쓴 뒤 1차 캐시 정합성 주의

동일 트랜잭션에서 jdbcTemplate.update(...)created_at을 갱신하면 영속성 컨텍스트가 오래된 값을 들고 있을 수 있습니다. 정렬/페이징 검증 신뢰도를 높이려면 flush 후 EntityManager.clear()를 호출해 주세요.

다음처럼 적용을 권장합니다:

@@
 import org.springframework.test.web.servlet.MvcResult;
+import jakarta.persistence.EntityManager;
 import org.springframework.transaction.annotation.Transactional;
@@
 class CommentShowAllApiTest {
@@
-    @Autowired private JdbcTemplate jdbcTemplate;
+    @Autowired private JdbcTemplate jdbcTemplate;
+    @Autowired private EntityManager em;
@@
         commentJpaRepository.flush();
         jdbcTemplate.update(
                 "UPDATE comments SET created_at = ? WHERE comment_id = ?",
                 Timestamp.valueOf(base.minusMinutes(40)), comment1.getCommentId());
@@
         jdbcTemplate.update(
                 "UPDATE comments SET created_at = ? WHERE comment_id = ?",
                 Timestamp.valueOf(base.minusMinutes(30)), comment1_1.getCommentId());
+        em.clear(); // JDBC 업데이트 반영 강제 반영

동일 패턴(주로 created_at 덮어쓰기)이 있는 다른 테스트 메서드에도 동일 적용을 권장합니다.

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

37-37: @transactional 전환 OK — JDBC 덮어쓰기 뒤 1차 캐시 정합성 보강 권장

savedBookJpaRepository.flush()jdbcTemplate.update(...)created_at을 덮어쓴 뒤 동일 트랜잭션에서 조회합니다. JPA 1차 캐시로 인해 정렬 검증이 드물게 흔들릴 수 있어 EntityManager.clear() 추가를 권장합니다.

아래처럼 보강해 주세요:

@@
-import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.annotation.Transactional;
+import jakarta.persistence.EntityManager;
@@
 class BookShowSavedListApiTest {
@@
-    @Autowired private JdbcTemplate jdbcTemplate;
+    @Autowired private JdbcTemplate jdbcTemplate;
+    @Autowired private EntityManager em;
@@
         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());
+        em.clear();

두 번째 테스트의 루프 업데이트 이후에도 동일하게 em.clear()를 한 번 호출해 주세요.

src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckShowApiTest.java (1)

39-39: 트랜잭션 전환 OK — JDBC 업데이트 전에 flush, 이후 clear 필요

attendanceCheckJpaRepository.save(...) 직후 곧바로 jdbcTemplate.update(...)created_at을 갱신하고 있어, INSERT가 아직 flush되지 않았다면 UPDATE가 0건 처리될 위험이 있습니다. 각 테스트에서 attendanceCheckJpaRepository.flush() 호출 후 업데이트하고, 마지막에 EntityManager.clear()도 권장합니다.

예시:

@@
-import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.annotation.Transactional;
+import jakarta.persistence.EntityManager;
@@
 class AttendanceCheckShowApiTest {
@@
-    @Autowired private JdbcTemplate jdbcTemplate;
+    @Autowired private JdbcTemplate jdbcTemplate;
+    @Autowired private EntityManager em;
@@
-        // ac1 -> ac2 -> ac3 순으로 작성
+        // ac1 -> ac2 -> ac3 순으로 작성
         LocalDateTime base = LocalDateTime.now();
+        attendanceCheckJpaRepository.flush();
         jdbcTemplate.update(
                 "UPDATE attendance_checks SET created_at = ? WHERE attendancecheck_id = ?",
                 Timestamp.valueOf(base.minusMinutes(30)), ac1.getAttendanceCheckId());
@@
                 Timestamp.valueOf(base.minusMinutes(10)), ac3.getAttendanceCheckId());
+        em.clear();

같은 패턴이 있는 페이징 테스트에도 동일 적용 부탁드립니다.

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

8-27: 선택: OpenAPI 스키마 메타데이터 추가 제안

클라이언트 합의를 돕기 위해 각 필드에 @Schema(description=..., example=...)를 부여하면 좋습니다(특히 userPercentage(0~100), currentVotes).

src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java (1)

35-35: 트랜잭션 전환 OK — soft delete 상태 갱신 시 flush 권장

jdbcTemplate.update("UPDATE users SET status...") 전에 userJpaRepository.flush()를 호출하고, 이후 EntityManager.clear()로 1차 캐시를 비워 두면 상태 기반 검증의 신뢰도가 올라갑니다.

패치 예시:

@@
-import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.annotation.Transactional;
+import jakarta.persistence.EntityManager;
@@
 class UserVerifyNicknameControllerTest {
@@
-    @Autowired private JdbcTemplate jdbcTemplate;
+    @Autowired private JdbcTemplate jdbcTemplate;
+    @Autowired private EntityManager em;
@@
-        UserJpaEntity deleteUser = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "노성준"));
+        UserJpaEntity deleteUser = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER, "노성준"));
+        userJpaRepository.flush();
         jdbcTemplate.update(
                 "UPDATE users SET status = ? WHERE user_id = ?",
                 StatusType.INACTIVE.name(), deleteUser.getUserId());
+        em.clear();
src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSingleApiTest.java (1)

59-61: 미사용 변수 제거

Category c1이 사용되지 않습니다. 불필요한 선언을 제거하면 가독성이 좋아집니다.

-        Category c1 = TestEntityFactory.createScienceCategory();
         tags = List.of(Tag.PHYSICS, Tag.GENERAL_SCIENCE);
src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSpecificUserApiTest.java (1)

347-349: DisplayName 오타(nit)

주석에 ‘특정 유선’ → ‘특정 유저’로 보입니다. 사소하지만 혼동 방지를 위해 수정 제안드립니다.

-                 * 정렬 조건
-                 * 특정 유선 공개 피드 최신순 조회
+                 * 정렬 조건
+                 * 특정 유저 공개 피드 최신순 조회
src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksApiTest.java (2)

66-74: 중복 setter 호출 제거

nicknameUpdatedAt이 연속으로 두 번 설정됩니다(두 번째 값으로 덮어씀). 의미 중복이므로 하나만 남기세요.

-                .nicknameUpdatedAt(LocalDate.now().minusMonths(7))
                 .nicknameUpdatedAt(LocalDate.now().minusMonths(1))

80-86: Redis 키 초기화 후 세팅 권장(nit)

테스트 순서에 따른 간헐적 실패를 막기 위해 set 전에 동일 키를 삭제하는 것이 안전합니다.

-        String json = objectMapper.writeValueAsString(bookRankInfos);
-        redisTemplate.opsForValue().set(detailKey, json);
+        String json = objectMapper.writeValueAsString(bookRankInfos);
+        redisTemplate.delete(detailKey);
+        redisTemplate.opsForValue().set(detailKey, json);
src/test/java/konkuk/thip/room/adapter/in/web/RoomShowMineApiTest.java (1)

560-566: 1페이지 응답에서 nextCursor를 동적으로 추출하세요

혼합 페이징 포맷 변경 시 하드코딩된 커서 때문에 테스트가 쉽게 깨집니다. 첫 페이지 응답에서 nextCursor를 읽어 사용하도록 변경하세요.

-        // 다음 페이지 커서: 첫 페이지의 마지막 레코드 = recruiting4
-        // 혼합 커서 형식 = priority|deadlineDate|roomId (priority: 진행=0, 모집=1; deadlineDate: 진행=endDate, 모집=startDate)
-        String nextCursor = "1|" + recruiting4.getStartDate() + "|" + recruiting4.getRoomId();
+        // 다음 페이지 커서는 1페이지 응답에서 동적으로 추출
+        String page1Body = page1.andReturn().getResponse().getContentAsString();
+        String nextCursor = com.jayway.jsonpath.JsonPath.read(page1Body, "$.data.nextCursor");
src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateApiTest.java (1)

249-252: 메서드 레벨 @transactional 중복 제거 가능

클래스에 이미 @transactional이 있으므로 Line 251의 메서드 레벨 @transactional은 의미상 중복입니다(특수 Propagation/Isolation이 아니라면). 제거해도 동작 동일합니다.

-    @Test
-    @DisplayName("방 생성에 성공하면, 방장의 정보가 DB에 저장된다.")
-    @Transactional      // RoomParticipant -> Room, User 의 manyToOne 지연로딩을 위해 추가
+    @Test
+    @DisplayName("방 생성에 성공하면, 방장의 정보가 DB에 저장된다.")
     void room_create_room_participant_save_success() throws Exception {
src/test/java/konkuk/thip/room/adapter/in/web/RoomJoinApiTest.java (1)

170-172: 취소 후 flush/clear 타이밍 적절

상태/인원수 재조회 시 1차 캐시 오염 방지에 효과적입니다. join 테스트에도 동일 패턴을 맞추면 일관성이 좋아집니다.

     void joinRoom_success() throws Exception {
@@
                 .andExpect(status().isOk());
 
+        em.flush();
+        em.clear();
+
         // 참여자 저장 확인
src/test/java/konkuk/thip/common/persistence/StatusFilterTest.java (2)

26-26: 트랜잭셔널 전환은 적절. 다만 JDBC 갱신 후 1차 캐시 동기화 필수

테스트 내에서 JdbcTemplate.update(...)로 DB를 직접 갱신한 뒤 같은 트랜잭션에서 JPA로 재조회합니다. 1차 캐시에 이미 관리 중인 엔티티가 있으면 상태 필터링/JPQL 결과가 왜곡될 수 있습니다. 각 갱신 구간 뒤에 em.flush(); em.clear();를 호출(또는 대상 엔티티 detach)해 주세요.

적용 예시(요지):

  • @PersistenceContext EntityManager em;
  • saveInactiveUser(...) 말미 또는 각 테스트의 JDBC 갱신 직후 em.flush(); em.clear(); 호출

플레이키 여부를 점검하려면 다음 테스트만 반복 실행해 주세요: default_find_by_id_method_does_not_execute_filtering, custom_find_active_by_id_method_does_execute_filtering.


210-230: 조인 기반 집계 테스트도 동일한 동기화 이슈 가능

inactiveUser를 JDBC로 INACTIVE 전환 후 같은 트랜잭션에서 QueryDSL 집계를 수행합니다. 위 동기화 조치가 없으면 count가 2→1로 줄지 않는 등 간헐 실패가 발생할 수 있습니다. 동일하게 flush/clear를 보장해 주세요.

src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteQueryPersistenceAdapter.java (1)

26-28: 동률 정렬 보강 제안

총 참여자 수 합계가 동일한 투표 간 정렬이 비결정적일 수 있습니다. 리포지토리 정렬에 vote.id.desc()를 타이브레이커로 추가해 주세요.

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

17-19: roomStatus 추가는 합리적. userPercentage null 가능성 검토 필요

RECRUITING 상태에서는 userPercentage가 의미 없을 수 있습니다. 현재 생성자에서 not-null을 강제하므로, 쿼리에서 coalesce(..., 0.0) 등으로 항상 값이 채워지도록 보장해주세요. 보장되지 않으면 테스트/런타임에서 IllegalArgumentException이 발생합니다.

Also applies to: 28-28


24-24: 오타/메시지 정합성

roomTitle 필드에 대해 "roomName must not be null" 메시지를 사용하고 있습니다. 필드명과 맞춰 주세요.

-Assert.notNull(roomTitle, "roomName must not be null");
+Assert.notNull(roomTitle, "roomTitle must not be null");
src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java (1)

172-184: userId 제거 및 recent 경로 위임 LGTM + 72시간 컷 보장 확인

  • 카테고리별 마감임박/인기 경로에서 userId 제거는 “내가 참여한 방도 포함” 변경(#5)과 일치합니다. Good.
  • findRoomsByCategoryOrderByRecent는 Repository 구현이 “생성 72시간 이내” + createdAt DESC + roomId ASC 보조정렬을 보장해야 합니다. 테스트로 커버해 주세요.

72시간 컷/정렬 보장을 위한 통합 테스트 추가를 권유드립니다:

  • given: 생성시각이 80h, 73h, 71h, 1h 인 4개 방
  • expect: 80h, 73h는 제외; 71h, 1h만 반환, createdAt DESC, 동률 시 roomId ASC

findRoomsByCategoryOrderByPopular 네이밍을 ...MemberCountDesc로 맞추면 가독성↑

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

1-1: 유효성 예외(400) 경로에 대한 부정 테스트 추가 제안

서비스 레벨 검증에 맞춘 부정 케이스 2종을 추가해 주세요.

  • keyword/category 모두 빈데 isAllCategory=false → 400
  • isAllCategory=true 이면서 keyword와 category 동시 지정 → 400

원한다면 실패 케이스 템플릿 제공해 드리겠습니다.

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

201-234: 모집중 방 포함 및 정렬 우선순위 검증 Good + deadlineDate 검증 보강 제안

  • 진행중(50%) → 모집중 순서 검증이 요구사항(#4)에 부합합니다.
  • 추가로, 모집중 항목의 deadlineDate가 비어있지 않음, 진행중 항목의 deadlineDate는 null(또는 미사용 값)임을 단언하면 응답 계약이 더 견고해집니다.

예:

  • recruiting: jsonPath("$.data.roomList[1].deadlineDate", not(isEmptyOrNullString()))
  • in-progress: jsonPath("$.data.roomList[0].deadlineDate").doesNotExist() 또는 value(nullValue()) (정책에 따라)
src/main/java/konkuk/thip/room/application/service/RoomSearchService.java (1)

65-77: 전체 카테고리 분기 OK(가독성 개선 여지)

isAllCategory || hasKeyword 조건은 명확합니다. 위 정규화 적용 시 keywordNorm을 그대로 전달하면 됩니다.

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

34-38: 카테고리별 정렬 검증 완료 — 네이밍 권고

  • userId 제거 및 recent 정렬 메서드 추가는 호출부/구현 모두 반영되어 PR 목표에 부합합니다 (호출부: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java; 인터페이스: src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepository.java).
  • 인기순(findRoomsByCategoryOrderByMemberCount)은 실제로 memberCount.desc()로 정렬됩니다 (구현: src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java — 예: lines ~358–362). 메서드명에 "...MemberCountDesc" 명시를 권고합니다.
  • 최근순(findRoomsByCategoryOrderByCreatedAtDesc)은 72시간 컷과 보조 정렬이 구현되어 있습니다 (구현: src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java — .and(room.createdAt.goe(LocalDateTime.now().minusHours(72))) 및 .orderBy(room.createdAt.desc(), room.roomId.desc())).
src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingOrExpiredDetailViewApiTest.java (1)

288-289: 설명 문구의 상태코드 오기(400 → 403)

실제 검증은 403 FORBIDDEN입니다. DisplayName을 맞춰 주세요.

-    @DisplayName("모임방에 속하지 않는 유저가 진행중인 모임방 상세조회를 요청한 경우, 400 error 발생한다.")
+    @DisplayName("모임방에 속하지 않는 유저가 상세조회를 요청하면 403 Forbidden이 발생한다.")
src/main/java/konkuk/thip/room/application/service/RoomShowPlayingOrExpiredDetailViewService.java (1)

56-74: roomParticipant null 필드 NPE 가드

currentPage 또는 userPercentage가 null일 경우 NPE/언캐스팅 위험이 있습니다. 기본값 가드를 추가해 주세요(테스트도 상수/0 기대치로 정렬).

-        return RoomPlayingOrExpiredDetailViewResponse.builder()
+        Integer safeCurrentPage = roomParticipant.getCurrentPage() == null ? 0 : roomParticipant.getCurrentPage();
+        Double safeUserPct = roomParticipant.getUserPercentage() == null ? 0.0 : roomParticipant.getUserPercentage();
+        return RoomPlayingOrExpiredDetailViewResponse.builder()
                 .isHost(roomParticipant.isHost())
@@
-                .currentPage(roomParticipant.getCurrentPage())
-                .userPercentage((int) roomParticipant.getUserPercentage())
+                .currentPage(safeCurrentPage)
+                .userPercentage((int) Math.max(0, Math.floor(safeUserPct)))
src/main/java/konkuk/thip/room/application/mapper/RoomParticipantQueryMapper.java (2)

23-41: 모집중 잔여시간 표기 ‘최대 1일’ 요구사항 확인 필요

RecruitingRoomFormatAfterTimeSimple은 ‘N일/N시간/N분’ 그대로 노출합니다. PR 목표(4) “모임홈에서는 최대 1일까지만” 요구를 충족하는지 확인해 주세요. 필요 시 1일 초과는 “1일”로 캡핑하는 로직을 추가하세요. 또한 dto.userPercentage()가 null일 가능성에 대한 가드도 권장합니다.

예시:

-        if (IN_PROGRESS.equals(dto.roomStatus())) {
-            userPercentage = dto.userPercentage().intValue();
+        if (IN_PROGRESS.equals(dto.roomStatus())) {
+            userPercentage = dto.userPercentage() == null ? 0 : dto.userPercentage().intValue();
         } else if (RECRUITING.equals(dto.roomStatus())) {
-            deadlineDate = DateUtil.RecruitingRoomFormatAfterTimeSimple(dto.startDate());
+            long days = java.time.temporal.ChronoUnit.DAYS.between(java.time.LocalDate.now(), dto.startDate());
+            deadlineDate = (days > 1) ? "1일" : DateUtil.RecruitingRoomFormatAfterTimeSimple(dto.startDate());
         }

14-18: 불필요한 MapStruct imports 옵션 정리

현재 default 메서드 내 직접 호출이라 @Mapper(imports = DateUtil.class)는 불필요합니다. 제거해 간결하게 유지하세요.

-@Mapper(
-        componentModel = "spring",
-        imports = DateUtil.class,
-        unmappedTargetPolicy = ReportingPolicy.IGNORE       // 명시적으로 매핑하지 않은 필드를 무시하도록 설정
-)
+@Mapper(componentModel = "spring",
+        unmappedTargetPolicy = ReportingPolicy.IGNORE)
src/main/java/konkuk/thip/room/application/service/RoomGetDeadlinePopularRecentService.java (3)

33-34: 사소한 스타일: 쉼표 뒤 공백 누락

가독성 위해 공백을 추가하세요.

-return RoomGetDeadlinePopularRecentResponse.of(deadlineRoomList, popularRoomList,recentRoomList);
+return RoomGetDeadlinePopularRecentResponse.of(deadlineRoomList, popularRoomList, recentRoomList);

19-21: DEFAULT_LIMIT(=4) 근거 확인

UI 카드 수/그리드 규격과 일치하는지 확인 바랍니다. 변경 가능성이 높다면 설정/프로퍼티로 추출해 주세요.


23-31: 세 가지 정렬 쿼리의 성능/인덱스 점검

  • deadline: (category, recruit_end_date) 혹은 (category, start_date) 관련 인덱스
  • popular: (category, participation_count DESC) 또는 (category, like_count DESC) 등 지표 컬럼 인덱스
  • recent: (category, created_at DESC) 인덱스
    조회량이 큰 홈 진입점이면 커버링 인덱스/최신 N개 파티셔닝 고려를 권장합니다.
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java (3)

42-47: 주석-구현 불일치: 메서드명/주석 정비 필요

현재 where 절은 RECRUITING만 포함합니다. 주석의 “모집중 + ACTIVE 공통 where” 표현은 혼선을 줍니다. “모집중 공통 where” 등으로 정리하거나 메서드명을 recruitingWhere로 변경 권장.


244-248: 불필요 캐스팅

(double) 0 대신 0.0 사용 권장. 불필요 캐스팅 제거로 가독성만 개선됩니다.


325-343: public 시그니처 변경(유저 배제 조건 제거) — 호출부 동기화 확인 / 인덱스 권고

  • userId 제거 및 공통 조건 메서드로 이관은 문제 없음. 호출부/인터페이스 동기화 확인:
    • RoomQueryPersistenceAdapter 호출: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:173,178
    • 인터페이스: src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepository.java:34,36
    • 구현체: src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java:326,346
  • 성능 권고: 카테고리 랭킹/정렬 쿼리는 호출 빈도 높을 수 있으니 아래 복합 인덱스 권장: (room_status, is_public, category, start_date ASC, member_count DESC, room_id ASC)
src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java (1)

109-116: 용어 통일: ‘완료된’ → ‘만료된’

도메인 상태가 EXPIRED(만료)인 만큼 스웨거 요약/설명도 “만료된”으로 통일 권장.

- summary = "진행중인/완료된 방 상세보기",
- description = "진행중인/완료된 방의 상세 정보를 조회합니다."
+ summary = "진행중인/만료된 방 상세보기",
+ description = "진행중인/만료된 방의 상세 정보를 조회합니다."
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira 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 104aa6b and 6eeed8a.

📒 Files selected for processing (71)
  • src/main/java/konkuk/thip/common/security/util/JwtUtil.java (1 hunks)
  • src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java (2 hunks)
  • src/main/java/konkuk/thip/common/util/DateUtil.java (1 hunks)
  • src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java (5 hunks)
  • src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetDeadlinePopularRecentResponse.java (1 hunks)
  • src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetDeadlinePopularResponse.java (0 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/RoomPlayingOrExpiredDetailViewResponse.java (1 hunks)
  • src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.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 (12 hunks)
  • src/main/java/konkuk/thip/room/application/mapper/RoomParticipantQueryMapper.java (2 hunks)
  • src/main/java/konkuk/thip/room/application/mapper/RoomQueryMapper.java (2 hunks)
  • src/main/java/konkuk/thip/room/application/port/in/RoomGetDeadlinePopularRecentUseCase.java (1 hunks)
  • src/main/java/konkuk/thip/room/application/port/in/RoomGetDeadlinePopularUseCase.java (0 hunks)
  • src/main/java/konkuk/thip/room/application/port/in/RoomShowPlayingDetailViewUseCase.java (0 hunks)
  • src/main/java/konkuk/thip/room/application/port/in/RoomShowPlayingOrExpiredDetailViewUseCase.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/out/RoomQueryPort.java (1 hunks)
  • src/main/java/konkuk/thip/room/application/port/out/dto/RoomParticipantQueryDto.java (3 hunks)
  • src/main/java/konkuk/thip/room/application/service/RoomGetDeadlinePopularRecentService.java (3 hunks)
  • src/main/java/konkuk/thip/room/application/service/RoomSearchService.java (5 hunks)
  • src/main/java/konkuk/thip/room/application/service/RoomShowPlayingOrExpiredDetailViewService.java (5 hunks)
  • src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteQueryPersistenceAdapter.java (2 hunks)
  • src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteQueryRepository.java (2 hunks)
  • src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteQueryRepositoryImpl.java (3 hunks)
  • src/main/java/konkuk/thip/roompost/application/port/out/VoteQueryPort.java (2 hunks)
  • src/main/java/konkuk/thip/roompost/application/service/validator/RoomPostAccessValidator.java (2 hunks)
  • src/test/java/konkuk/thip/book/adapter/in/web/BookChangeSavedApiTest.java (2 hunks)
  • src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchApiTest.java (5 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 (2 hunks)
  • src/test/java/konkuk/thip/book/adapter/in/web/BookSearchApiTest.java (2 hunks)
  • src/test/java/konkuk/thip/book/adapter/in/web/BookShowSavedListApiTest.java (2 hunks)
  • src/test/java/konkuk/thip/comment/adapter/in/web/CommentCreateControllerTest.java (0 hunks)
  • src/test/java/konkuk/thip/comment/adapter/in/web/CommentDeleteApiTest.java (7 hunks)
  • src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllApiTest.java (2 hunks)
  • src/test/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapterTest.java (0 hunks)
  • src/test/java/konkuk/thip/comment/domain/CommentTest.java (0 hunks)
  • src/test/java/konkuk/thip/common/persistence/StatusFilterTest.java (1 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/BasicFeedShowAllApiTest.java (2 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowMineApiTest.java (2 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSingleApiTest.java (2 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowSpecificUserApiTest.java (2 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedShowUserInfoApiTest.java (8 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java (0 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FollowingPriorityFeedShowAllApiTest.java (2 hunks)
  • src/test/java/konkuk/thip/feed/domain/FeedTest.java (0 hunks)
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateApiTest.java (1 hunks)
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomGetDeadlinePopularRecentApiTest.java (3 hunks)
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomGetHomeJoinedRoomsApiTest.java (4 hunks)
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomGetMemberListApiTest.java (2 hunks)
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomJoinApiTest.java (5 hunks)
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingOrExpiredDetailViewApiTest.java (14 hunks)
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomRecruitingDetailViewApiTest.java (2 hunks)
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomSearchApiTest.java (3 hunks)
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomShowMineApiTest.java (2 hunks)
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomVerifyPasswordApiTest.java (2 hunks)
  • src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckCreateApiTest.java (2 hunks)
  • src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckDeleteApiTest.java (4 hunks)
  • src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckShowApiTest.java (2 hunks)
  • src/test/java/konkuk/thip/roompost/adapter/in/web/RoomPostSearchApiTest.java (1 hunks)
  • src/test/java/konkuk/thip/roompost/adapter/in/web/VoteCreateApiTest.java (2 hunks)
  • src/test/java/konkuk/thip/roompost/application/service/VoteCreateServiceTest.java (2 hunks)
  • src/test/java/konkuk/thip/user/adapter/in/web/UserDeleteApiTest.java (4 hunks)
  • src/test/java/konkuk/thip/user/adapter/in/web/UserFollowApiTest.java (2 hunks)
  • src/test/java/konkuk/thip/user/adapter/in/web/UserIsFollowingApiTest.java (1 hunks)
  • src/test/java/konkuk/thip/user/adapter/in/web/UserShowFollowingsInFeedViewApiTest.java (2 hunks)
  • src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java (2 hunks)
  • src/test/java/konkuk/thip/user/adapter/in/web/UserUpdateApiTest.java (2 hunks)
  • src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java (2 hunks)
💤 Files with no reviewable changes (8)
  • src/test/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapterTest.java
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java
  • src/test/java/konkuk/thip/comment/adapter/in/web/CommentCreateControllerTest.java
  • src/test/java/konkuk/thip/comment/domain/CommentTest.java
  • src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetDeadlinePopularResponse.java
  • src/main/java/konkuk/thip/room/application/port/in/RoomShowPlayingDetailViewUseCase.java
  • src/test/java/konkuk/thip/feed/domain/FeedTest.java
  • src/main/java/konkuk/thip/room/application/port/in/RoomGetDeadlinePopularUseCase.java
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-07-23T17:41:55.507Z
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#101
File: src/test/java/konkuk/thip/comment/adapter/in/web/CommentControllerTest.java:118-265
Timestamp: 2025-07-23T17:41:55.507Z
Learning: CommentControllerTest는 댓글 생성 API의 검증 로직과 예외 상황만을 테스트하는 단위 테스트이며, 성공 케이스는 별도의 통합 테스트(CommentCreateAPITest)에서 다룬다.

Applied to files:

  • src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllApiTest.java
  • src/test/java/konkuk/thip/comment/adapter/in/web/CommentDeleteApiTest.java
📚 Learning: 2025-09-13T06:46:25.061Z
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#300
File: src/main/java/konkuk/thip/room/application/service/RoomJoinService.java:37-42
Timestamp: 2025-09-13T06:46:25.061Z
Learning: seongjunnoh 프로젝트에서 방 참여 상태 변경 API(RoomJoinService.changeJoinState)는 모집중인 방(RECRUITING)에서만 JOIN과 CANCEL이 모두 가능하며, 공통 검증에서 room.validateRoomRecruitExpired()를 사용하는 것이 올바른 구현이다.

Applied to files:

  • src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomGetDeadlinePopularRecentApiTest.java
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomGetHomeJoinedRoomsApiTest.java
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingOrExpiredDetailViewApiTest.java
📚 Learning: 2025-09-05T04:11:01.456Z
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#293
File: src/main/java/konkuk/thip/common/scheduler/RoomStateScheduler.java:20-0
Timestamp: 2025-09-05T04:11:01.456Z
Learning: RoomStateScheduler의 changeRoomStateToExpired()와 changeRoomStateToProgress()는 서로 다른 날짜 조건으로 완전히 다른 방들을 대상으로 하므로(end_date < today vs start_date <= today AND end_date >= today), 비동기 실행 순서와 관계없이 데이터 정합성에 문제가 없다.

Applied to files:

  • src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingOrExpiredDetailViewApiTest.java
🧬 Code graph analysis (5)
src/main/java/konkuk/thip/room/application/port/in/RoomShowPlayingOrExpiredDetailViewUseCase.java (1)
src/main/java/konkuk/thip/room/application/port/in/RoomShowPlayingDetailViewUseCase.java (1)
  • RoomShowPlayingDetailViewUseCase (5-8)
src/test/java/konkuk/thip/user/adapter/in/web/UserIsFollowingApiTest.java (2)
src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllApiTest.java (1)
  • SpringBootTest (36-356)
src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateApiTest.java (1)
  • SpringBootTest (39-279)
src/main/java/konkuk/thip/room/application/mapper/RoomParticipantQueryMapper.java (1)
src/main/java/konkuk/thip/common/util/DateUtil.java (1)
  • DateUtil (12-108)
src/test/java/konkuk/thip/room/adapter/in/web/RoomGetDeadlinePopularRecentApiTest.java (2)
src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateApiTest.java (1)
  • SpringBootTest (39-279)
src/test/java/konkuk/thip/room/adapter/in/web/RoomGetMemberListApiTest.java (1)
  • SpringBootTest (35-191)
src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingOrExpiredDetailViewApiTest.java (1)
src/main/java/konkuk/thip/common/util/DateUtil.java (1)
  • DateUtil (12-108)
⏰ 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

Comment on lines +44 to 45
@Parameter(description = "전체검색여부 (전체 검색에 해당할때만 true로 보내주세요) ", example = "true") @RequestParam(value = "isAllCategory", required = false, defaultValue = "false") final boolean isAllCategory,
@Parameter(description = "정렬 방식 (마감 임박 : deadline, 신청 인원 : memberCount)", example = "deadline") @RequestParam("sort") final String sort,
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

isAllCategory 플래그: 파라미터-로직 규칙 명확화 필요

권장 규칙:

  • isAllCategory=true → 카테고리 무시, 키워드 없어도 전체검색 허용.
  • isAllCategory=false → keyword 또는 category 중 최소 하나는 필수. 둘 다 빈 값이면 400 반환.
  • category와 isAllCategory=true가 함께 온 경우 서버에서 category 무시(로그로 경고).

RoomSearchQuery.of(...)가 위 규칙을 강제하는지 확인 부탁드립니다. 필요 시 검증 실패 응답 및 스웨거 설명에 예시 추가 권장.

서명 변경 검증:

원하시면 유효성 매트릭스(키워드/카테고리/isAllCategory 조합별 동작) 테스트 코드도 작성해 드립니다.

Also applies to: 52-52


🏁 Script executed:

#!/bin/bash
rg -nP 'RoomSearchQuery\.of\(' -C2

Length of output: 570


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n --hidden -S '\b(class|record|interface)\s+RoomSearchQuery\b' src || true
rg -n --hidden -S 'RoomSearchQuery\.of\(' src || true
files=$(rg -n --hidden -S '\b(class|record|interface)\s+RoomSearchQuery\b' src -l || true)
if [ -n "$files" ]; then
  for file in $files; do
    echo "---- $file (first 300 lines) ----"
    sed -n '1,300p' "$file" || true
  done
fi

Length of output: 984


isAllCategory 유효성 추가 — RoomSearchQuery.of가 현재 검증을 수행하지 않습니다

확인: src/main/java/konkuk/thip/room/application/port/in/dto/RoomSearchQuery.java의 of(...)는 단순히 레코드 인스턴스만 생성합니다.

  • 필수 규칙(반드시 구현):
    • isAllCategory=true → category 무시, keyword 없어도 전체검색 허용.
    • isAllCategory=false → keyword 또는 category 중 최소 하나 필수; 둘 다 비었으면 400 반환.
    • 요청에 category와 isAllCategory=true 동시 포함 시 category 무시하고 경고 로그 남김.
  • 권장 구현 위치: RoomSearchQuery.of(...) 내부에서 검증 후 예외 발생(컨트롤러에서 400으로 매핑) 또는 컨트롤러(파라미터 바인딩 지점)에서 사전검증.
  • 수정 대상(정확한 포인터):
    • src/main/java/konkuk/thip/room/application/port/in/dto/RoomSearchQuery.java (of 메서드)
    • src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java (파라미터 처리 — 약 44–52라인)
  • 추가: 스웨거 파라미터 설명에 동작 규칙/예시 명시 및 유효성 실패(400) 응답 문서화.
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/room/application/port/in/dto/RoomSearchQuery.java
update the of(...) factory to perform the described validation: if isAllCategory
is true, ignore any provided category (do not drop keyword) and return the
record while logging a warning when category was supplied; if isAllCategory is
false, require that at least one of keyword or category is non-empty and throw
an IllegalArgumentException (or a custom validation exception) when both are
empty; ensure the exception will be translated to HTTP 400 by the controller
advice or exception handler. In
src/main/java/konkuk/thip/room/adapter/in/web/RoomQueryController.java around
lines 44–52, adjust parameter handling/documentation: update Swagger @Parameter
descriptions to explain the validation rules/behavior and examples, remove
duplicate server-side validation (rely on RoomSearchQuery.of(...) for
enforcement) and ensure that any thrown validation exception is mapped to a 400
response in API docs.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (3)
src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingOrExpiredDetailViewApiTest.java (3)

60-86: EXPIRED/IN_PROGRESS 시나리오별 endDate가 일관되지 않음

saveScienceRoom()에서 endDate를 무조건 today+30으로 설정합니다. EXPIRED 케이스에서도 endDate가 미래가 되어 상태-날짜 불일치가 생깁니다. roomStatus에 따라 endDate를 분기하세요(또는 startDate 기반으로 계산).

 private RoomJpaEntity saveScienceRoom(String bookTitle, String isbn, String roomName, LocalDate startDate, RoomStatus roomStatus) {
@@
-        return roomJpaRepository.save(RoomJpaEntity.builder()
+        LocalDate computedEndDate =
+                (roomStatus == RoomStatus.EXPIRED)
+                        ? LocalDate.now().minusDays(1)   // 이미 종료
+                        : startDate.plusDays(30);        // 진행/모집 기준 기본 기간
+
+        return roomJpaRepository.save(RoomJpaEntity.builder()
                 .title(roomName)
@@
-                .startDate(startDate)
-                .endDate(LocalDate.now().plusDays(30))
+                .startDate(startDate)
+                .endDate(computedEndDate)
                 .recruitCount(10)
@@
                 .memberCount(4)
                 .roomStatus(roomStatus)
                 .build());

195-219: IN_PROGRESS 시나리오에서 startDate가 내일로 설정됨 → 상태/날짜 불일치

스케줄러/도메인 정의상 IN_PROGRESS는 start_date <= today AND end_date >= today여야 합니다. 현재 startDate가 LocalDate.now().plusDays(1)로 미래입니다. 오늘(또는 과거)로 조정하고, 기대값(progressStartDate)도 함께 맞춰 주세요.

예시(이 테스트에 대한 수정; 다른 IN_PROGRESS 테스트도 동일 패턴으로 교정 필요):

-        RoomJpaEntity room = saveScienceRoom("과학-책", "isbn1", "과학-방-1일뒤-활동시작", LocalDate.now().plusDays(1),IN_PROGRESS);
+        RoomJpaEntity room = saveScienceRoom("과학-책", "isbn1", "과학-방-오늘-활동시작", LocalDate.now(), IN_PROGRESS);
@@
-                .andExpect(jsonPath("$.data.roomName", is("과학-방-1일뒤-활동시작")))
+                .andExpect(jsonPath("$.data.roomName", is("과학-방-오늘-활동시작")))
@@
-                .andExpect(jsonPath("$.data.progressStartDate", is(DateUtil.formatDate(LocalDate.now().plusDays(1)))))
+                .andExpect(jsonPath("$.data.progressStartDate", is(DateUtil.formatDate(LocalDate.now()))))

적용 대상 메서드: get_playing_room_detail, get_playing_room_detail_host, get_playing_room_detail_not_belong_to_room, get_playing_room_detail_too_many_votes, get_playing_room_detail_no_votes.


93-101: oauth2Id가 모든 유저에서 동일 → 유니크 제약 시 충돌 위험

oauth2Id를 "oauth2Id"로 고정하면 유니크 인덱스가 있을 때 테스트가 실패할 수 있습니다. i를 붙여 유니크하게 생성하세요.

-                        .oauth2Id("oauth2Id")
+                        .oauth2Id("oauth2Id-" + i)
🧹 Nitpick comments (1)
src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingOrExpiredDetailViewApiTest.java (1)

165-176: LocalDate.now() 다중 호출로 인한 경계 시각(자정) 취약성

테스트 내에서 now를 여러 번 호출하면 자정 경계에서 불안정할 수 있습니다. 메서드 상단에서 final LocalDate today = LocalDate.now();로 고정해 사용하세요.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira 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 6eeed8a and cf038d1.

📒 Files selected for processing (1)
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingOrExpiredDetailViewApiTest.java (14 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-07-08T16:30:33.771Z
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을 사용해야 한다.

Applied to files:

  • src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingOrExpiredDetailViewApiTest.java
📚 Learning: 2025-09-05T04:11:01.456Z
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#293
File: src/main/java/konkuk/thip/common/scheduler/RoomStateScheduler.java:20-0
Timestamp: 2025-09-05T04:11:01.456Z
Learning: RoomStateScheduler의 changeRoomStateToExpired()와 changeRoomStateToProgress()는 서로 다른 날짜 조건으로 완전히 다른 방들을 대상으로 하므로(end_date < today vs start_date <= today AND end_date >= today), 비동기 실행 순서와 관계없이 데이터 정합성에 문제가 없다.

Applied to files:

  • src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingOrExpiredDetailViewApiTest.java
📚 Learning: 2025-09-13T06:46:25.061Z
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#300
File: src/main/java/konkuk/thip/room/application/service/RoomJoinService.java:37-42
Timestamp: 2025-09-13T06:46:25.061Z
Learning: seongjunnoh 프로젝트에서 방 참여 상태 변경 API(RoomJoinService.changeJoinState)는 모집중인 방(RECRUITING)에서만 JOIN과 CANCEL이 모두 가능하며, 공통 검증에서 room.validateRoomRecruitExpired()를 사용하는 것이 올바른 구현이다.

Applied to files:

  • src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingOrExpiredDetailViewApiTest.java
🧬 Code graph analysis (1)
src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingOrExpiredDetailViewApiTest.java (1)
src/main/java/konkuk/thip/common/util/DateUtil.java (1)
  • DateUtil (12-108)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (2)
src/test/java/konkuk/thip/room/adapter/in/web/RoomPlayingOrExpiredDetailViewApiTest.java (2)

143-189: EXPIRED 시나리오 startDate 수정은 LGTM

기존 코멘트에서 지적된 EXPIRED의 startDate 문제는 해결되었습니다. 다만 위 saveScienceRoom()의 endDate 보정은 함께 필요합니다(상태/날짜 정합성).


37-39: enum의 static import 사용은 가독성에 도움됨

EXPIRED/IN_PROGRESS를 정적 임포트한 부분은 가독성 측면에서 좋습니다.

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 +37 to 38
@Transactional
@DisplayName("[통합] 가장 많이 검색된 책 조회 API 통합 테스트")
Copy link
Contributor

Choose a reason for hiding this comment

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

트랜잭션 어노테이션을 달더라도 Redis는 롤백이 되지 않는다는거야??

Comment on lines +99 to +100
em.flush();
em.clear();
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
Member Author

Choose a reason for hiding this comment

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

엇 이거 저번에 저희 카페에서도 있었던 이슈고 그 성준띠니가 작성한 노션에 적어뒀는데

이게아마 저희그때 댓글 삭제 시에 그 소프트 딜리트 같은 경우(게시물 타입말고 @SQLdelete어노테이션을 이용하여 삭제할때)에는 트랜잭션 어노테이션을 사용할경우 같은 트랜잭션 내에서 조회할 시, 바로 반영이안되서 flush,clear를 명시해야만 컨텍스트에 상태반영이되어서 정상적으로 테스트가 진행되었던거같은데

해당 사항때문에 초기화햇습니닷!!

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 +28 to +31
@Value("${jwt.access-token-expiration}")
private long tokenExpiredMs;
@Value("${jwt.signup-token-expiration}")
private long signupTokenExpiredMs;
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 71 to 92
public static String RecruitingRoomFormatAfterTimeSimple(LocalDate date) {
LocalDateTime now = LocalDateTime.now();
LocalDateTime dateTime = date.atStartOfDay();
Duration d = Duration.between(now, dateTime);

if (d.isNegative() || d.isZero()) {
return "??";
}

long days = d.toDays();
if (days > 0) {
return days + "일";
}

long hours = d.toHours();
if (hours >= 1) {
return hours + "시간";
}

long minutes = d.toMinutes();
return minutes + "분";
}
Copy link
Contributor

Choose a reason for hiding this comment

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

p3: 혹시 메서드 네이밍이 대문자로 시작하는것은 의도하신건가요??

Copy link
Member Author

Choose a reason for hiding this comment

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

엇 별생각없이 적었습니다 ㅎ..

Comment on lines 18 to 21
@Schema(description = "진행중인 방에서 유저의 방 진행도(ex. \"35\"), 모집중인 방은 쓰레기값이 넘어갑니다. 무시해주세요.")
int userPercentage,
@Schema(description = "모집중인 방에서 방 모집 마감일까지 남은 시간 (ex. \"3일\"), 진행중인 방은 쓰레기값이 넘어갑니다. 무시해주세요.")
String deadlineDate // 방 모집 마감일 (~일/시 형식)
Copy link
Contributor

Choose a reason for hiding this comment

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

p3: Swagger 명세에서 description 내부에 example 값을 직접 넣기보다는 example 파라미터를 활용해 예시를 지정할 수 있습니다. 따라서 굳이 escape 문자를 사용하지 않아도 될 것 같습니다.

@Schema(
    description = "진행중인 방에서 유저의 방 진행도, 모집중인 방은 쓰레기값이 넘어갑니다. 무시해주세요.",
    example = "35"
)
int userPercentage,

Comment on lines +243 to +250
// 1. 상태가 IN_PROGRESS이면 진행률 높은 순, 모집중이면 0(기본값)로 처리해 후순위로 빠지게 함
new CaseBuilder()
.when(room.roomStatus.eq(IN_PROGRESS))
.then(participant.userPercentage)
.otherwise((double) 0)
.desc(),
room.startDate.asc(), // 2. 활동 시작일 빠른 순
room.roomId.asc() // 3. 방 아이디 작은 순(오름차순)
Copy link
Contributor

Choose a reason for hiding this comment

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

이거는 마지막 정렬조건은 사전순에서 id로 대체된거죠?? 카톡으로 보긴했던 것 같은데 노션에는 여전히 사전순으로 되어 있어서 확인차 여쭤봅니다~!

Copy link
Member Author

Choose a reason for hiding this comment

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

넵넵 id로 바뀌었습니다!!

Comment on lines 365 to 383
@Override
public List<RoomQueryDto> findRoomsByCategoryOrderByCreatedAtDesc(Category category, int limit) {
return queryFactory
.select(new QRoomQueryDto(
room.roomId,
book.imageUrl,
room.title,
room.recruitCount,
room.memberCount,
room.startDate,
room.roomStatus
))
.from(room)
.join(room.bookJpaEntity, book)
.where(findDeadlinePopularRecentRoomCondition(category)
.and(room.createdAt.goe(LocalDateTime.now().minusHours(72)))) //생성된지 72시간 이내
.orderBy(room.createdAt.desc(), room.roomId.desc())
.limit(limit)
.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: LocalDateTime.now() 호출을 쿼리 안에서 바로 쓰는 대신, 서비스 로직에서 호출하여 메서드 파라미터로 넘겨주고 여기서는 단순 비교만 하는 게 어떨까요?
이렇게 하면 추후에 쿼리 성능 체크 또는 단위 테스트할 때 조금더 수월할 것 같습니다~

Comment on lines 57 to 96
private CursorBasedList<RoomQueryDto> executeRecruitingRoomSearch(RoomSearchQuery query, Category category, RoomSearchSortParam sortParam, Cursor cursor) {
CursorBasedList<RoomQueryDto> result = null;
boolean isAllCategory = query.isAllCategory();
String keyword = query.keyword();
boolean isKeywordEmpty = (keyword == null || keyword.trim().isEmpty());

// 빈 키워드이면서 isAllCategory가 true인 경우는 전체 조회를 위해 빈 문자열을 사용하고, 그렇지 않으면 그대로 keyword를 사용
String effectiveKeyword = isKeywordEmpty && isAllCategory ? "" : keyword;

if (category == null) {
switch (sortParam) {
case DEADLINE:
return roomQueryPort.searchRecruitingRoomsByDeadline(query.keyword(), cursor);
case MEMBER_COUNT:
return roomQueryPort.searchRecruitingRoomsByMemberCount(query.keyword(), cursor);
// 전체 카테고리 중에서
// 1) 전체검색(isAllCategory=true)이거나
// 2) 키워드가 비어있지 않은 경우
// 해당 조건 모두 포함해서 키워드 기반 검색 또는 전체 검색 수행
if (isAllCategory || !isKeywordEmpty) {
switch (sortParam) {
case DEADLINE:
return roomQueryPort.searchRecruitingRoomsByDeadline(effectiveKeyword, cursor);
case MEMBER_COUNT:
return roomQueryPort.searchRecruitingRoomsByMemberCount(effectiveKeyword, cursor);
}
}
} else {
switch (sortParam) {
case DEADLINE:
return roomQueryPort.searchRecruitingRoomsWithCategoryByDeadline(query.keyword(), category, cursor);
case MEMBER_COUNT:
return roomQueryPort.searchRecruitingRoomsWithCategoryByMemberCount(query.keyword(), category, cursor);
if (isAllCategory && isKeywordEmpty) {
// isAllCategory가 true이고, 키워드가 비어있으면
// 특정 카테고리 내에서 '전체 조회'를 의미함 즉, 키워드 없이 카테고리 필터만 적용해서 전체 방 조회
switch (sortParam) {
case DEADLINE:
return roomQueryPort.searchRecruitingRoomsWithCategoryByDeadline("", category, cursor);
case MEMBER_COUNT:
return roomQueryPort.searchRecruitingRoomsWithCategoryByMemberCount("", category, cursor);
}
} else if (!isAllCategory) {
// isAllCategory가 false인 경우 (전체검색 아님)
// category가 존재하고 키워드는 있거나 빈 문자열이어도 키워드 기반 조회 수행
switch (sortParam) {
case DEADLINE:
return roomQueryPort.searchRecruitingRoomsWithCategoryByDeadline(effectiveKeyword, category, cursor);
case MEMBER_COUNT:
return roomQueryPort.searchRecruitingRoomsWithCategoryByMemberCount(effectiveKeyword, category, cursor);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

p3: 플래그 3개로 분기처리를 수행하다보니 코드 가독성이 약간 떨어지는 느낌이 드는 것 같은데 RoomSearchMode 같은 상수를 도입해서 검색 모드를 나눠보는 거 어떨까요? 검색 모드를 한번 결정해두고 내부적으로는 정적으로 분기처리하면 훨씬 가독성이 올라갈 것 같아요! 제가 생각한 예시 코드입니다. 저는 정렬 조건 -> 검색 모드로 분기 처리를 해봤는데, 현재 희진님 코드처럼 검색 모드 -> 정렬 조건 이렇게 분기처리해도 괜찮을 것 같습니다!

검색 모드 Enum

public enum RoomSearchMode {
        GLOBAL_BY_KEYWORD_OR_ALL, // 카테고리 없음: 전체 또는 키워드 기반
        CATEGORY_ALL,             // 카테고리 있음 + 전체조회 (키워드 비어있고 isAllCategory=true)
        CATEGORY_BY_KEYWORD       // 카테고리 있음 + 키워드 기반(또는 전체가 아님)
    }

    private RoomSearchMode determineSearchMode(Category category, boolean isAllCategory, String keyword) {
        final boolean isKeywordEmpty = isEmpty(keyword);

        if (category == null) {
            // 카테고리 없음 → 전체 또는 키워드 기반
            // 유효성 검증에서 이미 조합을 보장하므로 그대로 처리
            return RoomSearchMode.GLOBAL_BY_KEYWORD_OR_ALL;
        }

        // 카테고리 있음
        if (isAllCategory && isKeywordEmpty) {
            return RoomSearchMode.CATEGORY_ALL;
        }
        return RoomSearchMode.CATEGORY_BY_KEYWORD;
    }

    private String resolveEffectiveKeyword(RoomSearchMode mode, String keyword) {
        final boolean isKeywordEmpty = isEmpty(keyword);
        return switch (mode) {
            case GLOBAL_BY_KEYWORD_OR_ALL ->
                // 전체(isAllCategory=true)이며 키워드 비어있을 수 있으므로 빈 문자열 허용
                    isKeywordEmpty ? "" : keyword.trim();
            case CATEGORY_ALL ->
                // 카테고리 전체 조회: 키워드 강제 빈 문자열
                    "";
            case CATEGORY_BY_KEYWORD ->
                // 키워드가 비어있을 수도 있지만, 의미적으로 "전체가 아님"이므로
                // 저장 계층에서 빈 문자열이면 전체와 동일해질 수 있음 → 그대로 전달(정책 유지)
                    isKeywordEmpty ? "" : keyword.trim();
        };
    }

    private boolean isEmpty(String s) {
        return s == null || s.trim().isEmpty();
    }

서비스 로직

    @Override
    @Transactional
    public RoomSearchResponse searchRecruitingRooms(RoomSearchQuery query) {
        // 1) 파라미터 파싱/검증
        final RoomSearchSortParam sortParam = RoomSearchSortParam.from(query.sortStr());
        validateSearchParams(query.keyword(), query.isAllCategory(), query.categoryStr());
        final Category category = validateCategory(query.categoryStr());

        // 2) 검색 모드/키워드 결정
        final RoomSearchMode mode = determineSearchMode(category, query.isAllCategory(), query.keyword());
        final String effectiveKeyword = resolveEffectiveKeyword(mode, query.keyword());

        // 3) 커서 생성
        final Cursor cursor = Cursor.from(query.cursorStr(), DEFAULT_PAGE_SIZE);

        // 4) 실행 (정렬 기준별 단일 switch)
        final CursorBasedList<RoomQueryDto> result = executeSearchMode(mode, sortParam, effectiveKeyword, category, cursor);

        // 5) 최근 검색어 저장
        recentSearchCreateManager.saveRecentSearchByUser(
                query.userId(), query.keyword(), RecentSearchType.ROOM_SEARCH, query.isFinalized()
        );

        // 6) 응답 매핑
        return new RoomSearchResponse(
                roomQueryMapper.toRoomSearchResponse(result.contents()),
                result.nextCursor(),
                result.isLast()
        );
    }

    private CursorBasedList<RoomQueryDto> executeSearchMode(
            RoomSearchMode mode,
            RoomSearchSortParam sort,
            String keyword,
            Category category,
            Cursor cursor
    ) {
        return switch (sort) {
            case DEADLINE -> executeByDeadline(mode, keyword, category, cursor);
            case MEMBER_COUNT -> executeByMemberCount(mode, keyword, category, cursor);
            default -> throw new BusinessException(
                    API_INVALID_PARAM,
                    new IllegalArgumentException("지원하지 않는 정렬 기준입니다: " + sort)
            );
        };
    }

    private CursorBasedList<RoomQueryDto> executeByDeadline(
            RoomSearchMode mode, String keyword, Category category, Cursor cursor
    ) {
        return switch (mode) {
            case GLOBAL_BY_KEYWORD_OR_ALL -> roomQueryPort.searchRecruitingRoomsByDeadline(keyword, cursor);
            case CATEGORY_ALL -> roomQueryPort.searchRecruitingRoomsWithCategoryByDeadline("", category, cursor);
            case CATEGORY_BY_KEYWORD ->
                    roomQueryPort.searchRecruitingRoomsWithCategoryByDeadline(keyword, category, cursor);
        };
    }

    private CursorBasedList<RoomQueryDto> executeByMemberCount(
            RoomSearchMode mode, String keyword, Category category, Cursor cursor
    ) {
        return switch (mode) {
            case GLOBAL_BY_KEYWORD_OR_ALL -> roomQueryPort.searchRecruitingRoomsByMemberCount(keyword, cursor);
            case CATEGORY_ALL -> roomQueryPort.searchRecruitingRoomsWithCategoryByMemberCount("", category, cursor);
            case CATEGORY_BY_KEYWORD ->
                    roomQueryPort.searchRecruitingRoomsWithCategoryByMemberCount(keyword, category, cursor);
        };
    }

Copy link
Member Author

Choose a reason for hiding this comment

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

너무 좋습니다!! 확실히 가독성이 더 좋아질것같아욧 수정하겠씁니다~~

Comment on lines +110 to +126
private void validateSearchParams(String keyword, boolean isAllCategory, String categoryStr) {
boolean isKeywordEmpty = (keyword == null || keyword.trim().isEmpty());
boolean isCategoryEmpty = (categoryStr == null || categoryStr.trim().isEmpty());

// 키워드와 카테고리 둘 다 없을 때 isAllCategory가 true여야 함
if (isKeywordEmpty && isCategoryEmpty && !isAllCategory) {
throw new BusinessException(API_INVALID_PARAM,
new IllegalArgumentException("검색어와 카테고리가 없을 경우, 전체 검색을 명시하는 isAllCategory=true 옵션이 필요합니다."));
}

// 기존 예외 : 키워드 있는데 isAllCategory=true 이면서 특정 카테고리 존재 불가
if (isAllCategory && !isKeywordEmpty && !isCategoryEmpty) {
throw new BusinessException(API_INVALID_PARAM,
new IllegalArgumentException("키워드가 있을 때 특정 카테고리 검색과 전체검색(isAllCategory=true)을 동시에 사용할 수 없습니다."));
}
}

Copy link
Contributor

Choose a reason for hiding this comment

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

LGTM

Comment on lines +56 to +71
StringBuilder sb = new StringBuilder(contents.length());

// 필요한 전체 반복 횟수 계산
int repeat = originalLength / blurLen;
// 블러 문자열 인덱스
int blurIndex = 0;

StringBuilder sb = new StringBuilder(originalLength);
for (int i = 0; i < contents.length(); i++) {
char ch = contents.charAt(i);

// 몫 만큼 반복
for (int i = 0; i < repeat + 1; i++) {
sb.append(BLURRED_STRING);
// 특수문자/공백일 경우 그대로 append
if (Character.isWhitespace(ch) || isSpecialCharacter(ch)) {
sb.append(ch);
} else {
// 나머지 문자들은 모두 치환
sb.append(BLURRED_STRING.charAt(blurIndex));
blurIndex = (blurIndex + 1) % blurLen; // 순환
}
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

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (3)
src/main/java/konkuk/thip/common/util/DateUtil.java (1)

49-69: UX: 음수/0초일 때 "??" 노출은 사용자 혼란 유발

모집 마감 이후(또는 당일 00:00 이후) "??"가 그대로 노출될 수 있습니다. 사용자-facing 문자열이므로 “마감” 등 명확한 문구로 바꾸는 것을 권장합니다.

적용 예:

-        if (d.isNegative() || d.isZero()) {
-            return "??";
-        }
+        if (d.isNegative() || d.isZero()) {
+            return "마감";
+        }
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java (1)

197-254: 정렬 우선순위(상태) 및 커서 정합성 수정 필요

파일: src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java (Lines 197–254)

문제: 요구사항은 IN_PROGRESS가 항상 RECRUITING보다 앞서야 하며(IN_PROGRESS 내: 진행도 desc → 시작일 asc → id asc, 이후 RECRUITING: 시작일 asc → id asc). 현재 구현은 진행도(or 0)만 1차 정렬키로 사용해 IN_PROGRESS(진행도 0)가 RECRUITING과 섞일 수 있고, 커서(userPercentage, startDate, roomId)가 정렬 키와 일치하지 않아 페이지 경계에서 누락·중복이 발생할 가능성이 있음.

권장 수정(간결):

  • 정렬 최상위 키로 상태 우선순위(statusPriority: IN_PROGRESS=0, RECRUITING=1) 추가.
  • 메서드 시그니처에 statusPriorityCursor(Integer) 추가하고 커서 비교에도 상태 우선순위를 포함해 키셋 정렬(상태 → 진행도(또는 0) → 시작일 → id)과 정확히 일치시키기.
  • progressOrZero를 CaseBuilder로 계산해 상태에 따라 진행도 또는 0을 사용하고, orderBy는 statusPriority.asc(), progressOrZero.desc(), room.startDate.asc(), room.roomId.asc()로 정렬.

예시 패치(요지 — 참고용):

-    public List<RoomParticipantQueryDto> findHomeJoinedRoomsByUserPercentage(
-            Long userId,
-            Double userPercentageCursor,
-            LocalDate startDateCursor,
-            Long roomIdCursor,
-            int pageSize) {
+    public List<RoomParticipantQueryDto> findHomeJoinedRoomsByUserPercentage(
+            Long userId,
+            Integer statusPriorityCursor, // 0: IN_PROGRESS, 1: RECRUITING
+            Double userPercentageCursor,
+            LocalDate startDateCursor,
+            Long roomIdCursor,
+            int pageSize) {
@@
-        where.and(room.roomStatus.eq(IN_PROGRESS)
-                .or(room.roomStatus.eq(RECRUITING)));
+        BooleanExpression playing    = room.roomStatus.eq(IN_PROGRESS);
+        BooleanExpression recruiting = room.roomStatus.eq(RECRUITING);
+        where.and(playing.or(recruiting));
@@
-        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))
-                                            )
-                            )
-            );
-        }
+        NumberExpression<Integer> statusPriority =
+                new CaseBuilder().when(playing).then(0).otherwise(1);
+        NumberExpression<Double> progressOrZero =
+                new CaseBuilder().when(playing).then(participant.userPercentage).otherwise(0.0);
+
+        if (statusPriorityCursor != null && userPercentageCursor != null && startDateCursor != null && roomIdCursor != null) {
+            where.and(
+                statusPriority.gt(statusPriorityCursor)
+                .or(statusPriority.eq(statusPriorityCursor)
+                   .and(progressOrZero.lt(userPercentageCursor))
+                   .or(statusPriority.eq(statusPriorityCursor)
+                      .and(progressOrZero.eq(userPercentageCursor))
+                      .and(room.startDate.gt(startDateCursor))
+                      .or(statusPriority.eq(statusPriorityCursor)
+                         .and(progressOrZero.eq(userPercentageCursor))
+                         .and(room.startDate.eq(startDateCursor))
+                         .and(room.roomId.gt(roomIdCursor))
+                      )
+                   )
+                )
+            );
+        }
@@
-                .orderBy(
-                        new CaseBuilder()
-                                .when(room.roomStatus.eq(IN_PROGRESS))
-                                .then(participant.userPercentage)
-                                .otherwise((double) 0)
-                                .desc(),
-                        room.startDate.asc(),
-                        room.roomId.asc()
-                )
+                .orderBy(
+                        statusPriority.asc(),
+                        progressOrZero.desc(),
+                        room.startDate.asc(),
+                        room.roomId.asc()
+                )

인터페이스/서비스/컨트롤러 시그니처 동기화 필요. 빠른 대응이 어려우면 우선 정렬의 최상위 키(statusPriority)만이라도 추가해 IN_PROGRESS가 항상 앞에 오도록 보장하라.

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

103-109: 카테고리 파싱 시 공백 트림 누락

validateSearchParams에서는 trim()으로 공백만 입력된 카테고리를 빈값으로 처리하지만, 여기서는 isEmpty()만 사용합니다. " " 입력이 Category.from()로 넘어가며 예외가 뒤늦게 발생할 수 있습니다. 일관되게 trim()을 적용하세요.

-    private Category validateCategory(String categoryStr) {
-        if (categoryStr == null || categoryStr.isEmpty()) {
+    private Category validateCategory(String categoryStr) {
+        if (categoryStr == null || categoryStr.trim().isEmpty()) {
             return null;
         }
-
-        return Category.from(categoryStr);
+        return Category.from(categoryStr.trim());
     }
♻️ Duplicate comments (2)
src/main/java/konkuk/thip/common/util/DateUtil.java (1)

49-69: 네이밍 케이스 수정 반영 확인됨

메서드명이 lowerCamelCase로 정리된 점 좋습니다. 과거 대문자 시작 네이밍 지적 사항이 해소되었습니다.

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

41-44: 이전 코멘트 반영: 키워드 NULL/공백 정규화 완료

RoomSearchMode 기반으로 effectiveKeyword를 도입하여 과거의 null 전파/NPE 위험이 사라졌습니다. 잘 정리되었습니다.

🧹 Nitpick comments (11)
src/main/java/konkuk/thip/common/util/DateUtil.java (1)

71-93: Simple 변형도 동일한 "??" 이슈 및 중복 로직

  • Simple 메서드도 음수/0에 “??”를 반환합니다. 동일하게 “마감”으로 교체 권장.
  • 두 메서드가 거의 동일한 계산 로직을 중복합니다. 포맷 옵션만 다르므로 내부 헬퍼로 공통화하면 유지보수성이 좋아집니다.

“??” 교체 예:

-        if (d.isNegative() || d.isZero()) {
-            return "??";
-        }
+        if (d.isNegative() || d.isZero()) {
+            return "마감";
+        }

공통화 아이디어(개념 스케치):

private static Duration untilStartOfDay(LocalDate date) {
    return Duration.between(LocalDateTime.now(), date.atStartOfDay());
}
// 옵션: suffix(" 남음"), 임박 문구 사용 여부, 분 노출 여부 등
src/main/java/konkuk/thip/room/application/service/RoomShowRecruitingDetailViewService.java (1)

67-67: 필드 의미 불일치 가능성: recruitEndDate에 startDate 기반 남은 시간

recruitEndDate라는 필드명에 DateUtil.recruitingRoomFormatAfterTime(room.getStartDate()) 결과(문자열 “n일 남음/마감 임박”)를 설정하고 있습니다. 실제 “모집 마감일(날짜)”을 의미한다면 타입·명칭이 어긋나고, “모집 마감까지 남은 시간”을 의미한다면 필드명을 recruitRemaining 등으로 바꾸는 것이 혼선을 줄입니다. 또한 정책상 모집 마감이 활동 시작일 00:00인지도 함께 확인 필요합니다.

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

35-41: 최근 72시간 API 시그니처 합리적(Clock/now 주입) — 문서화만 보강해주세요

findRoomsByCategoryOrderByCreatedAtDesc(Category, LocalDateTime now, int limit)로 기준시각을 인자로 받는 방향 좋습니다. 재현성/타임존 이슈 방지에 유리합니다. 메서드 주석으로 “최근=now 기준 72시간 이내”를 명시하면 더 명확합니다.

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

169-183: 추천 목록 정렬 안정화(tie-breaker) 제안

orderBy(room.startDate.asc())만으로는 동일 startDate 간 결과가 불안정할 수 있습니다. 2차 정렬 키로 room.roomId.asc()를 추가하세요.

-                .orderBy(room.startDate.asc())
+                .orderBy(room.startDate.asc(), room.roomId.asc())

365-384: 최근 생성 72시간 쿼리: 시그니처와 조건 모두 적절 — 인덱스 고려 권장

now.minusHours(72) 비교와 createdAt desc, roomId desc 정렬 구성 좋습니다. 운영 효율을 위해 (category, roomStatus, isPublic, createdAt desc) 복합 인덱스 검토를 권합니다.


417-421: 카테고리 null 처리 확인 필요

findDeadlinePopularRecentRoomCondition(category)에서 room.category.eq(category)를 바로 사용합니다. 호출부에서 category가 항상 비null임이 보장되지 않으면 전체 조회 시 필터가 category IS NULL로 오동작합니다. 호출 컨벤션을 명시하거나 null-safe 분기를 추가하세요.

src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (1)

102-102: 오타/문구 톤 불일치 및 선행 공백 제거 필요

메시지 앞에 공백이 포함되어 있고, 기존 enum 전반의 문체(“…입니다/…할 수 없습니다.”)와 다르게 구어체(“…가능해요.”)입니다. 사용자-facing 일관성을 위해 톤과 공백을 정리해 주세요.

-    ROOM_IS_EXPIRED(HttpStatus.BAD_REQUEST, 100008, " 완료된 모임방에서는 기존 기록에 대한 조회만 가능해요."),
+    ROOM_IS_EXPIRED(HttpStatus.BAD_REQUEST, 100008, "완료된 모임방에서는 기존 기록만 조회할 수 있습니다."),
src/main/java/konkuk/thip/room/application/service/RoomGetDeadlinePopularRecentService.java (2)

36-36: 사소한 스타일 수정: 쉼표 뒤 공백

가독성을 위해 공백을 추가하세요.

-        return RoomGetDeadlinePopularRecentResponse.of(deadlineRoomList, popularRoomList,recentRoomList);
+        return RoomGetDeadlinePopularRecentResponse.of(deadlineRoomList, popularRoomList, recentRoomList);

27-34: Clock 주입: LocalDateTime.now() 대신 Clock 사용

RoomGetDeadlinePopularRecentService에서 LocalDateTime.now()를 직접 호출하지 말고 Clock을 생성자 주입해 LocalDateTime.now(clock)으로 대체하세요.

+import java.time.Clock;
 import java.time.LocalDateTime;
 
 @Service
 @RequiredArgsConstructor
 public class RoomGetDeadlinePopularRecentService implements RoomGetDeadlinePopularRecentUseCase {
 
     private final RoomQueryPort roomQueryPort;
     private final RoomQueryMapper roomQueryMapper;
+    private final Clock clock;
@@
-        LocalDateTime now = LocalDateTime.now();
+        LocalDateTime now = LocalDateTime.now(clock);

검증: 레포지토리 구현에서 이미 72시간 윈도우를 적용하고 있습니다 — src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java (조건: .and(room.createdAt.goe(now.minusHours(72)))).

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

86-89: 키워드 전달 일관화(미세 리팩터링)

CATEGORY_ALL에서만 리터럴 ""를 넘기면 가독성이 떨어집니다. keyword(이 케이스에서 이미 "")를 그대로 전달해 일관성을 유지하세요.

-            case CATEGORY_ALL -> roomQueryPort.searchRecruitingRoomsWithCategoryByDeadline("", category, cursor);
+            case CATEGORY_ALL -> roomQueryPort.searchRecruitingRoomsWithCategoryByDeadline(keyword, category, cursor);
@@
-            case CATEGORY_ALL -> roomQueryPort.searchRecruitingRoomsWithCategoryByMemberCount("", category, cursor);
+            case CATEGORY_ALL -> roomQueryPort.searchRecruitingRoomsWithCategoryByMemberCount(keyword, category, cursor);

Also applies to: 96-99


111-127: 검증 규칙 보완 제안(선택)

현재 규칙은 핵심 케이스를 커버합니다. 추가로 아래 조합들에 대한 기대 동작을 명시/테스트로 고정하면 좋겠습니다.

  • category!=null, isAllCategory=false, keyword empty → 카테고리 전체검색으로 해석 여부
  • category=null, isAllCategory=true, keyword provided → 전역 키워드 검색 허용 여부

명확화되면 예외 메시지에 “요청 파라미터 조합(카테고리/키워드/isAllCategory)”을 포함해 디버깅을 돕는 것도 좋습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira 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 cf038d1 and ca8feae.

📒 Files selected for processing (12)
  • src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (1 hunks)
  • src/main/java/konkuk/thip/common/util/DateUtil.java (2 hunks)
  • src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetHomeJoinedListResponse.java (2 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 (12 hunks)
  • src/main/java/konkuk/thip/room/application/mapper/RoomParticipantQueryMapper.java (2 hunks)
  • src/main/java/konkuk/thip/room/application/port/in/dto/RoomSearchMode.java (1 hunks)
  • src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java (2 hunks)
  • src/main/java/konkuk/thip/room/application/service/RoomGetDeadlinePopularRecentService.java (2 hunks)
  • src/main/java/konkuk/thip/room/application/service/RoomSearchService.java (4 hunks)
  • src/main/java/konkuk/thip/room/application/service/RoomShowRecruitingDetailViewService.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java
  • src/main/java/konkuk/thip/room/application/mapper/RoomParticipantQueryMapper.java
  • src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java
  • src/main/java/konkuk/thip/room/adapter/in/web/response/RoomGetHomeJoinedListResponse.java
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-13T06:46:25.061Z
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#300
File: src/main/java/konkuk/thip/room/application/service/RoomJoinService.java:37-42
Timestamp: 2025-09-13T06:46:25.061Z
Learning: seongjunnoh 프로젝트에서 방 참여 상태 변경 API(RoomJoinService.changeJoinState)는 모집중인 방(RECRUITING)에서만 JOIN과 CANCEL이 모두 가능하며, 공통 검증에서 room.validateRoomRecruitExpired()를 사용하는 것이 올바른 구현이다.

Applied to files:

  • src/main/java/konkuk/thip/common/exception/code/ErrorCode.java
  • src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java
🧬 Code graph analysis (1)
src/main/java/konkuk/thip/room/application/service/RoomShowRecruitingDetailViewService.java (1)
src/main/java/konkuk/thip/common/util/DateUtil.java (1)
  • DateUtil (12-108)
⏰ 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 (5)
src/main/java/konkuk/thip/common/util/DateUtil.java (1)

49-69: 모집 마감 시점 정의 재확인 필요: atStartOfDay()

date.atStartOfDay() 기준이라 같은 ‘시작일’의 당일 00:00 이후에는 즉시 마감 처리됩니다. 정책이 “시작 전날 23:59:59까지 모집”인지, “시작일 00:00에 마감”인지 확인 부탁드립니다. 전자라면 date.atStartOfDay()가 맞고, “시작일 하루 끝(23:59:59)까지”라면 date.plusDays(1).atStartOfDay() 등을 고려해야 합니다.

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

67-67: API 변경 반영 LGTM

DateUtil 메서드 리네임 반영이 올바르게 적용되었습니다.

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

387-391: ISBN 검색에서도 공개방 필터 필요

현재 RECRUITING만 체크합니다. 공개방만 노출되어야 한다면 isPublic 조건을 추가하세요.

-        BooleanExpression baseCondition = room.bookJpaEntity.isbn.eq(isbn)
-                .and(room.roomStatus.eq(RECRUITING)); // 모집중인 방
+        BooleanExpression baseCondition = room.bookJpaEntity.isbn.eq(isbn)
+                .and(room.roomStatus.eq(RECRUITING))
+                .and(room.isPublic.isTrue()); // 공개방만
⛔ Skipped due to learnings
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#287
File: src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java:37-41
Timestamp: 2025-09-01T10:29:31.377Z
Learning: seongjunnoh 프로젝트에서 방 검색 시에는 공개방과 비공개방을 모두 포함하여 노출하는 것이 요구사항이다. isPublic 필터는 추천/인기방 등 특정 컨텍스트에서만 적용된다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#300
File: src/main/java/konkuk/thip/room/application/service/RoomJoinService.java:37-42
Timestamp: 2025-09-13T06:46:25.061Z
Learning: seongjunnoh 프로젝트에서 방 참여 상태 변경 API(RoomJoinService.changeJoinState)는 모집중인 방(RECRUITING)에서만 JOIN과 CANCEL이 모두 가능하며, 공통 검증에서 room.validateRoomRecruitExpired()를 사용하는 것이 올바른 구현이다.

42-47: 공개방 필터 누락: 검색 API에서 비공개 방 노출 위험

recruitingActiveWhere()가 RECRUITING만 체크하고 있어, 이를 사용하는 검색 메서드에서 비공개 방까지 노출될 수 있습니다. 공개 방만 노출되어야 한다면 isPublic 조건을 포함해야 합니다.

적용 예:

-    /** 모집중 + ACTIVE 공통 where */
+    /** 모집중 공개방 공통 where */
     private BooleanBuilder recruitingActiveWhere() {
         BooleanBuilder where = new BooleanBuilder();
-        where.and(room.roomStatus.eq(RECRUITING));
+        where.and(room.roomStatus.eq(RECRUITING))
+             .and(room.isPublic.isTrue());
         return where;
     }
⛔ Skipped due to learnings
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#287
File: src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java:37-41
Timestamp: 2025-09-01T10:29:31.377Z
Learning: seongjunnoh 프로젝트에서 방 검색 시에는 공개방과 비공개방을 모두 포함하여 노출하는 것이 요구사항이다. isPublic 필터는 추천/인기방 등 특정 컨텍스트에서만 적용된다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#300
File: src/main/java/konkuk/thip/room/application/service/RoomJoinService.java:37-42
Timestamp: 2025-09-13T06:46:25.061Z
Learning: seongjunnoh 프로젝트에서 방 참여 상태 변경 API(RoomJoinService.changeJoinState)는 모집중인 방(RECRUITING)에서만 JOIN과 CANCEL이 모두 가능하며, 공통 검증에서 room.validateRoomRecruitExpired()를 사용하는 것이 올바른 구현이다.
src/main/java/konkuk/thip/room/application/port/in/dto/RoomSearchMode.java (1)

5-45: 검색 모드/키워드 정규화 설계 좋습니다

모드 결정과 키워드 정규화 분리를 통해 분기 복잡도가 낮아졌습니다. 이전 NPE/NULL 전파 리스크를 구조적으로 제거한 점 확인했습니다.

이 enum에 대한 파라미터 조합 테스트(카테고리 null/존재 × isAllCategory T/F × 키워드 공백/존재)를 파라미터라이즈드 테스트로 추가하면 회귀를 막는 데 유용합니다. 필요 시 예시 테스트 스켈레톤 제공 가능합니다.

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.

오 테스트 코드에 전부 트랜잭션 설정하도록 변경하는 것까지 해주셨군요!! 제가 하려고 했는데 감사합니다!!

노션 정리를 꼼꼼하게 해주셔서 리뷰하기 편했습니다!! 제가 처음 구현한 부분의 코드 수정이 좀 있었는데 감사합니다!!

리뷰 몇개 추가해보았는데, 확인부탁드립니다!

Comment on lines +5 to +8
public enum RoomSearchMode{
GLOBAL_BY_KEYWORD_OR_ALL, // 카테고리 없음: 전체 또는 키워드 기반
CATEGORY_ALL, // 카테고리 있음 + 전체조회 (키워드 비어있고 isAllCategory=true)
CATEGORY_BY_KEYWORD; // 카테고리 있음 + 키워드 기반(또는 전체가 아님)
Copy link
Collaborator

Choose a reason for hiding this comment

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

LGTM

public BaseResponse<RoomSearchResponse> searchRecruitingRooms(
@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 = "전체검색여부 (전체 검색에 해당할때만 true로 보내주세요) ", example = "true") @RequestParam(value = "isAllCategory", required = false, defaultValue = "false") final boolean isAllCategory,
Copy link
Collaborator

Choose a reason for hiding this comment

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

p3 : 혹시 isAllCategory 라는 request param 은 FE 분들과 사전 상의가 된 네이밍일까요??

Copy link
Member Author

Choose a reason for hiding this comment

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

넵 희용띠니와 상의해서 전체 파라미터를 나타내는 변수로 정했습니다!!

Comment on lines 86 to 87
case CATEGORY_ALL -> roomQueryPort.searchRecruitingRoomsWithCategoryByDeadline("", category, cursor);
case CATEGORY_BY_KEYWORD ->
Copy link
Collaborator

Choose a reason for hiding this comment

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

p2 : "" 이라는 빈 문자열을 주입하는게 아니라 keyword 를 바로 전달하는건 어떤가요? 이미 "" 로 사전처리를 해주셨으니 그대로 전달받은 인자를 주입하는게 좋을 것 같습니다 (전체 검색에 해당하는 경우의 keyword 조작은 모두 RoomSearchMode 가 책임지도록)

이렇게 되면 CATEGORY_ALL, CATEGORY_BY_KEYWORD 에 대한 분기처리를 합쳐야 할 것 같네요.

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 97 to 98
case CATEGORY_ALL -> roomQueryPort.searchRecruitingRoomsWithCategoryByMemberCount("", category, cursor);
case CATEGORY_BY_KEYWORD ->
Copy link
Collaborator

Choose a reason for hiding this comment

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

p2 : 이 부분도 keyword 를 그대로 주입하는게 좋을 것 같습니다

List<RoomParticipant> findByRoomId = roomParticipantCommandPort.findAllByRoomId(roomId);
RoomParticipants roomParticipants = RoomParticipants.from(findByRoomId);
// 2. Room과 연관된 RoomParticipant 조회
RoomParticipant roomParticipant = roomParticipantCommandPort.getByUserIdAndRoomIdOrThrow(userId, roomId);
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 -70 to +67
.memberCount(roomParticipants.calculateMemberCount())
.memberCount(room.getMemberCount())
Copy link
Collaborator

Choose a reason for hiding this comment

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

LGTM

.progressStartDate(DateUtil.formatDate(room.getStartDate()))
.progressEndDate(DateUtil.formatDate(room.getEndDate()))
.recruitEndDate(DateUtil.RecruitingRoomFormatAfterTime(room.getStartDate()))
.recruitEndDate(DateUtil.recruitingRoomFormatAfterTime(room.getStartDate()))
Copy link
Collaborator

Choose a reason for hiding this comment

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

수정 감사합니다!!

.from(room)
.join(room.bookJpaEntity, book)
.where(findDeadlinePopularRecentRoomCondition(category)
.and(room.createdAt.goe(now.minusHours(72))))
Copy link
Collaborator

Choose a reason for hiding this comment

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

p3 : service 에서 최근 몇 시간 내에 생성된 방을 보여줄 것인지를 정하고, 이를 영속성 계층까지 파라미터로 던지는건 어떤가요?
'최근 72 시간 내에 생성된 방을 보여준다' 라는 것은 단순 조회가 아니라 비즈니스 로직이라고 생각하는데, service 가 이 값에 대한 변경 책임을 가지도록 하는것이 더 좋을 것 같습니다!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
src/main/java/konkuk/thip/room/application/service/RoomSearchService.java (1)

51-54: 최근검색어 저장: 공백/NULL 차단 여부 재확인 필요

호출부에서 원문 keyword를 전달하고 있어 Manager 단에서 trim/빈값 스킵이 반드시 필요합니다. 과거 코멘트와 동일 제안입니다.

  • 확인 요청: RecentSearchCreateManager.saveRecentSearchByUser(...)가
    • !isFinalized 시 no-op,
    • keyword==null 또는 trim 후 빈 문자열이면 저장을 생략
      하도록 구현/테스트 되어 있나요? (DB 컬럼 nullable=false 고려)
src/main/java/konkuk/thip/room/application/service/RoomGetDeadlinePopularRecentService.java (1)

26-39: LocalDateTime.now() 직접 호출 → Clock 주입으로 테스트/타임존 안정화

  • 서비스 레이어에서도 Clock을 주입하면 단위테스트 재현성과 타임존 차이 이슈를 줄일 수 있습니다. (이전 논의 연장선)
@@
-import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import java.time.Clock;
@@
 @RequiredArgsConstructor
 public class RoomGetDeadlinePopularRecentService implements RoomGetDeadlinePopularRecentUseCase {
 
     private final RoomQueryPort roomQueryPort;
     private final RoomQueryMapper roomQueryMapper;
+    private final Clock clock; // 시스템 기본 Clock을 스프링 빈으로 주입하거나 구성
 
@@
-        LocalDateTime now = LocalDateTime.now();
+        LocalDateTime now = LocalDateTime.now(clock);
         LocalDateTime recentCutoff = now.minusHours(RECENT_HOURS);

또, DEFAULT_LIMIT=4가 기획 값인지 한 번만 확인 부탁드립니다.

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

325-343: 데드라인 목록 정렬 타이브레이커 명확화(사전순→id) 확인

  • startDate ASC → memberCount DESC → id ASC 순 정렬로 일관성 좋습니다. (이전 코멘트에서 “사전순→id로 변경” 합의 사항과 일치)
🧹 Nitpick comments (9)
src/main/java/konkuk/thip/room/application/service/RoomSearchService.java (5)

34-35: 조회용 서비스는 readOnly 트랜잭션으로 전환하세요

불필요한 쓰기 컨텍스트/더티체킹 비용을 줄일 수 있습니다.

-    @Transactional
+    @Transactional(readOnly = true)
     public RoomSearchResponse searchRecruitingRooms(RoomSearchQuery query) {

81-88: 카테고리 모드에서 category=null 방어 로직 추가 권장

determineSearchMode가 보증하더라도 방어적으로 체크하면 회귀에 강합니다.

     private CursorBasedList<RoomQueryDto> executeByDeadline(
             RoomSearchMode mode, String keyword, Category category, Cursor cursor
     ) {
+        if ((mode == RoomSearchMode.CATEGORY_ALL || mode == RoomSearchMode.CATEGORY_BY_KEYWORD) && category == null) {
+            throw new BusinessException(
+                API_INVALID_PARAM,
+                new IllegalArgumentException("카테고리 모드에서 category 가 null 입니다.")
+            );
+        }
         return switch (mode) {
             case GLOBAL_BY_KEYWORD_OR_ALL -> roomQueryPort.searchRecruitingRoomsByDeadline(keyword, cursor);
             case CATEGORY_ALL, CATEGORY_BY_KEYWORD -> roomQueryPort.searchRecruitingRoomsWithCategoryByDeadline(keyword, category, cursor);
         };
     }

90-97: 동일 방어 로직을 멤버수 정렬에도 추가

위와 동일하게 처리하면 일관적입니다.

     private CursorBasedList<RoomQueryDto> executeByMemberCount(
             RoomSearchMode mode, String keyword, Category category, Cursor cursor
     ) {
+        if ((mode == RoomSearchMode.CATEGORY_ALL || mode == RoomSearchMode.CATEGORY_BY_KEYWORD) && category == null) {
+            throw new BusinessException(
+                API_INVALID_PARAM,
+                new IllegalArgumentException("카테고리 모드에서 category 가 null 입니다.")
+            );
+        }
         return switch (mode) {
             case GLOBAL_BY_KEYWORD_OR_ALL -> roomQueryPort.searchRecruitingRoomsByMemberCount(keyword, cursor);
             case CATEGORY_ALL, CATEGORY_BY_KEYWORD -> roomQueryPort.searchRecruitingRoomsWithCategoryByMemberCount(keyword, category, cursor);
         };
     }

99-105: 카테고리 문자열 trim 누락 — 공백만 입력 시 예외 가능

validateSearchParams는 trim으로 공백을 빈값으로 보지만, 여기선 그대로 Category.from에 전달되어 예외가 날 수 있습니다. 일관되게 trim 처리하세요.

-    private Category validateCategory(String categoryStr) {
-        if (categoryStr == null || categoryStr.isEmpty()) {
-            return null;
-        }
-
-        return Category.from(categoryStr);
-    }
+    private Category validateCategory(String categoryStr) {
+        if (categoryStr == null) {
+            return null;
+        }
+        String normalized = categoryStr.trim();
+        if (normalized.isEmpty()) {
+            return null;
+        }
+        return Category.from(normalized);
+    }

107-122: 검증 메시지 OK — 정책 주석/테스트로 고정 권장

에러 메시지가 명확합니다. 추가로, 정책 변화가 잦은 영역이라 조합별 단위테스트(파라미터 매트릭스)로 스냅샷을 남겨 주세요.

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

183-185: 최근 목록 포트 위임 OK — createdAfter 의미 명시 제안

  • createdAfter 경계(포함/미포함)와 타임존 기준(서버/DB)을 Javadoc에 명시하면 호출 측 혼동을 줄일 수 있습니다. 현재 쿼리는 createdAt.goe(createdAfter)로 “경계 포함”입니다.
 /**
  * 카테고리별 최근 생성 방 조회.
+ * @param createdAfter 이 시각 이상(포함)의 createdAt만 조회합니다. (DB/서버 타임존 기준 일치 필요)
  * @param limit 반환 최대 개수
  */
src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java (1)

40-44: 포트 시그니처 브레이킹 체인지 — 호출부/테스트 일제 정리 확인 요청

  • userId 제거 및 recent 추가로 컴파일 타임 브레이킹 체인지입니다. 구현체(어댑터), 서비스, API 레이어와 테스트가 모두 정리되었는지 확인 부탁드립니다.

또한, 정렬의 세부 타이브레이커를 인터페이스 주석에 문서화하면(예: deadline: startDate ASC, memberCount DESC, id ASC / popular: memberCount DESC, startDate ASC, id ASC / recent: createdAt DESC, id DESC) 차후 변경 추적이 쉬워집니다.

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

22-22: RECENT_HOURS(72) 설정값 주입 고려

  • 상수 하드코딩 대신 설정(or Feature Flag) 주입으로 운영 중 조정 가능성을 확보하는 것을 권장합니다. 기본값 72 유지 + 프로퍼티 오버라이드 방식이 무난합니다.
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java (1)

365-386: 최근 목록 쿼리 성능 주의: createdAt 인덱스 제안

  • where(createdAt ≥ :createdAfter) + orderBy(createdAt DESC, id DESC)은 최근 72시간 범위라도 트래픽/데이터량 증가 시 스캔 비용이 커질 수 있습니다. 생성일 컬럼(및 카테고리+생성일 복합) 인덱스를 권장합니다.

예시(DDL):

  • 단일: CREATE INDEX idx_room_created_at ON room(created_at DESC);
  • 복합: CREATE INDEX idx_room_category_created_at ON room(category, created_at DESC);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira 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 ca8feae and c987296.

📒 Files selected for processing (6)
  • 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 (12 hunks)
  • src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java (2 hunks)
  • src/main/java/konkuk/thip/room/application/service/RoomGetDeadlinePopularRecentService.java (1 hunks)
  • src/main/java/konkuk/thip/room/application/service/RoomSearchService.java (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepository.java
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-13T06:46:25.082Z
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#300
File: src/main/java/konkuk/thip/room/application/service/RoomJoinService.java:37-42
Timestamp: 2025-09-13T06:46:25.082Z
Learning: seongjunnoh 프로젝트에서 방 참여 상태 변경 API(RoomJoinService.changeJoinState)는 모집중인 방(RECRUITING)에서만 JOIN과 CANCEL이 모두 가능하며, 공통 검증에서 room.validateRoomRecruitExpired()를 사용하는 것이 올바른 구현이다.

Applied to files:

  • src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (13)
src/main/java/konkuk/thip/room/application/service/RoomSearchService.java (5)

3-3: LGTM — 예외코드/모드 도입으로 의도 명확

BusinessException + API_INVALID_PARAM, RoomSearchMode 도입으로 정책 표현력이 좋아졌습니다.

Also applies to: 10-10, 21-22


36-39: 파라미터 검증 로직 전반 OK — 조합 매트릭스 테스트 추가 요청

주요 금지 조합을 테스트로 고정해 주세요. 특히 아래 허용/금지 의도 확인 부탁드립니다.

  • 허용 예상:
    • category=null, keyword="", isAllCategory=true
    • category=null, keyword=K, isAllCategory=(true|false)
    • category=C, keyword="", isAllCategory=(true|false) ← 의도적으로 둘 다 허용인지 확인 필요
    • category=C, keyword=K, isAllCategory=false
  • 금지 예상:
    • category=null, keyword="", isAllCategory=false
    • category=C, keyword=K, isAllCategory=true

41-44: 모드 기반 키워드 정규화 적용 Good

모드에 책임을 몰아 일관성이 올라갔습니다.


48-49: 단일 실행 진입점으로 단순화 Good

정렬 → 실행 분리 깔끔합니다.


64-79: 스위치 분기 기본값에서 명시적 예외 처리 Good

예외 코드도 적절합니다.

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

45-46: 모집중 필터 공통화 OK

  • recruitingActiveWhere에서 RECRUITING 고정은 본 파일 내 모집중 검색군에 일관되게 적용되어 좋습니다.

177-180: 추천 목록에 모집중 필터 추가 LGTM

  • 상세 화면 추천에서 진행중/비공개/자기 방 제외 조건이 명확합니다.

262-269: 모집중 참여목록 커서/정렬 OK

  • startDate ASC, roomId ASC 키셋 정렬은 안정적입니다.

277-284: 진행중 참여목록 커서/정렬 OK

  • endDate를 커서로 사용하는 설계가 만료 임박순에 부합합니다.

291-307: 혼합(진행+모집) 조회의 우선순위 키 도입 굿

  • priority(진행중=0, 모집중=1) → cursor(date) → id 정렬 체계가 요구사항과 잘 맞습니다.

346-363: 인기 목록 정렬 타이브레이커 OK

  • memberCount DESC → startDate ASC → id ASC로 안정 정렬 보장.

419-423: 공통 조건(카테고리+모집중+공개) 일원화 LGTM

  • deadline/popular/recent 3종에 동일 필터가 적용되어 응답 일관성이 확보됩니다.
src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java (1)

173-180: userId 제거에 따른 시그니처 변경 호출부 반영 확인 — 승인

검사 결과 호출부는 다음 파일들에서만 발견되며 모두 (Category, int) 시그니처로 일치합니다: src/main/java/konkuk/thip/room/application/port/out/RoomQueryPort.java, src/main/java/konkuk/thip/room/application/service/RoomGetDeadlinePopularRecentService.java, src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java.

Comment on lines +210 to +212
where.and(room.roomStatus.eq(IN_PROGRESS)
.or(room.roomStatus.eq(RECRUITING))); // 활동: IN_PROGRESS 상태, 모집: RECRUITING

Copy link

@coderabbitai coderabbitai bot Sep 16, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

홈 참여 목록 정렬: IN_PROGRESS 우선 보장이 불완전함(진행률=0% 사례)

  • 현재 정렬은 “진행중이면 userPercentage, 아니면 0”을 1차 키로 사용합니다. 진행률 0%인 진행중 방은 0으로 묶여 모집중과 동일 우선순위가 되어 startDate가 빠른 모집중 방이 먼저 나오는 경우가 생깁니다. 기획(“진행중 > 모집중”)에 어긋날 수 있어 우선순위 키(진행중=0, 모집중=1)를 1차로, 진행률은 2차로 두는 것을 권장합니다. 또한 진행률 NULL 안전(coalesce) 처리도 함께 권장합니다.

적용안:

@@
-        return queryFactory
+        NumberExpression<Integer> priorityExpr =
+                new CaseBuilder()
+                        .when(room.roomStatus.eq(IN_PROGRESS)).then(0)
+                        .otherwise(1);
+
+        NumberExpression<Double> progressScore =
+                new CaseBuilder()
+                        .when(room.roomStatus.eq(IN_PROGRESS))
+                        .then(participant.userPercentage.coalesce(0.0))
+                        .otherwise(0.0);
+
+        return queryFactory
@@
-                .orderBy(
-                        // 1. 상태가 IN_PROGRESS이면 진행률 높은 순, 모집중이면 0(기본값)로 처리해 후순위로 빠지게 함
-                        new CaseBuilder()
-                                .when(room.roomStatus.eq(IN_PROGRESS))
-                                .then(participant.userPercentage)
-                                .otherwise((double) 0)
-                                .desc(),
-                        room.startDate.asc(), // 2. 활동 시작일 빠른 순
-                        room.roomId.asc()  // 3. 방 아이디 작은 순(오름차순)
-                )
+                .orderBy(
+                        priorityExpr.asc(),     // 1) 진행중 먼저, 모집중 다음
+                        progressScore.desc(),   // 2) 진행중 내에서 진행률 높은 순
+                        room.startDate.asc(),   // 3) 활동 시작일 빠른 순
+                        room.roomId.asc()       // 4) 방 아이디 오름차순
+                )

Also applies to: 243-251

🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java
around lines 210-212 (and similarly lines 243-251), the current ORDER BY uses a
single primary key of "if IN_PROGRESS then userPercentage else 0" which groups
IN_PROGRESS rooms with 0% together with RECRUITING rooms; change ordering to use
an explicit priority key first (e.g., CASE WHEN roomStatus = IN_PROGRESS THEN 0
ELSE 1 END) so IN_PROGRESS always ranks ahead of RECRUITING, then use
COALESCE(userPercentage, 0) as the secondary key for descending progress, and
finally keep startDate as a tertiary key; implement the CASE expression and
COALESCE in the query's orderBy clause accordingly.

Copy link
Collaborator

Choose a reason for hiding this comment

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

아니야. 모든 방은 매일 자정에 startDate 를 기준으로 모집중인 방 -> 진행중인 방 으로 변경돼. 따라서 모집중인 방이 진행중인 방보다 startDate 가 빠를수는 없어

Copy link

Choose a reason for hiding this comment

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

@seongjunnoh 맞습니다! 스케줄러가 매일 자정에 startDate 기준으로 방 상태를 자동 변경하는 비즈니스 로직을 놓쳤네요.

그렇다면 현재 정렬 로직이 올바르게 동작합니다:

  • 모집중인 방: startDate가 오늘 이후 (미래)
  • 진행중인 방: startDate가 오늘 이전/당일 (과거/현재)

따라서 진행률 0%인 진행중 방도 모집중인 방보다 항상 startDate가 빠르거나 같아서, 제가 우려했던 정렬 문제는 실제로 발생할 수 없겠네요. 비즈니스 로직을 충분히 고려하지 못한 분석이었습니다. 감사합니다!

🐰
/| /|
( .. )
o
(")(")


✏️ Learnings added
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#304
File: src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomQueryRepositoryImpl.java:210-212
Timestamp: 2025-09-16T13:33:07.266Z
Learning: seongjunnoh 프로젝트에서 방의 상태는 스케줄러에 의해 매일 자정에 startDate를 기준으로 자동 변경된다 (RECRUITING → IN_PROGRESS). 따라서 모집중인 방의 startDate는 항상 진행중인 방의 startDate보다 크거나 같으며, 이는 정렬 로직 분석 시 반드시 고려해야 하는 중요한 비즈니스 제약사항이다.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#300
File: src/main/java/konkuk/thip/room/application/service/RoomJoinService.java:37-42
Timestamp: 2025-09-13T06:46:25.082Z
Learning: seongjunnoh 프로젝트에서 방 참여 상태 변경 API(RoomJoinService.changeJoinState)는 모집중인 방(RECRUITING)에서만 JOIN과 CANCEL이 모두 가능하며, 공통 검증에서 room.validateRoomRecruitExpired()를 사용하는 것이 올바른 구현이다.

Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#293
File: src/main/java/konkuk/thip/common/scheduler/RoomStateScheduler.java:20-0
Timestamp: 2025-09-05T04:11:01.456Z
Learning: RoomStateScheduler의 changeRoomStateToExpired()와 changeRoomStateToProgress()는 서로 다른 날짜 조건으로 완전히 다른 방들을 대상으로 하므로(end_date < today vs start_date <= today AND end_date >= today), 비동기 실행 순서와 관계없이 데이터 정합성에 문제가 없다.

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#285
File: src/main/java/konkuk/thip/room/adapter/out/jpa/RoomStatus.java:1-7
Timestamp: 2025-08-31T05:25:14.845Z
Learning: seongjunnoh는 enum 의존성에 대해 유연한 접근을 선호하며, 도메인→어댑터 레이어 참조와 같은 아키텍처 layering 원칙보다 실용적인 구현을 우선시한다.

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#278
File: src/main/java/konkuk/thip/TestTokenController.java:0-0
Timestamp: 2025-08-24T09:33:52.982Z
Learning: seongjunnoh는 Spring의 ConditionalOnProperty 동작 원리를 정확히 이해하고 있으며, 보안 이슈에 대해서도 실질적인 위험성을 기준으로 판단하는 실용적 접근을 선호한다.

Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#278
File: src/main/java/konkuk/thip/common/exception/code/ErrorCode.java:210-211
Timestamp: 2025-08-24T09:40:48.595Z
Learning: seongjunnoh는 HTTP 상태 코드 선택에 대해 기술적 근거와 코드베이스 내 일관성을 중요하게 생각하며, 구체적인 사례 분석을 통한 설명을 선호한다.

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.371Z
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 단일 커서 방식을 선호하며, 복합 키 기반 커서보다 구현 단순성과 성능을 우선시한다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
src/main/java/konkuk/thip/recentSearch/application/service/manager/RecentSearchCreateManager.java (4)

19-21: 키워드 검증/정규화 보완 제안

현재 trim만 적용되어 내부 연속 공백, 유니코드 정규화, 길이 초과가 그대로 저장될 수 있습니다. hasText 사용과(가독성) 길이 가드 추가를 제안합니다. DB 컬럼 길이와 정렬 규칙(collation)에 맞춰 조정 부탁.

-        if (keyword == null || keyword.trim().isEmpty()) return;
-        String normalized = keyword.trim();
+        if (!StringUtils.hasText(keyword)) return;
+        String normalized = keyword.trim();
+        // 예: 100자 제한(컬럼 길이에 맞게 조정)
+        if (normalized.length() > 100) normalized = normalized.substring(0, 100);

추가 import:

import org.springframework.util.StringUtils;

17-21: userId/type 유효성 확인 누락

null 값이 포트/영속 계층에서 NPE·제약 위반을 유발할 수 있습니다. 빠르게 실패하도록 검증 추가를 권장합니다.

     public void saveRecentSearchByUser(Long userId, String keyword, RecentSearchType type, boolean isFinalized) {
         if (!isFinalized) return; // 검색완료일 경우에 최근검색어 추가
-        if (keyword == null || keyword.trim().isEmpty()) return;
+        if (userId == null || type == null) throw new IllegalArgumentException("userId/type must not be null");
+        if (keyword == null || keyword.trim().isEmpty()) return;
         String normalized = keyword.trim();

21-21: 대소문자/로케일 정규화 고려(선택)

영문 검색어의 중복 방지를 위해 소문자 정규화를 고려해 주세요(한글에는 영향 거의 없음). DB collation이 case-insensitive라면 불필요합니다.

-        String normalized = keyword.trim();
+        String normalized = keyword.trim();
+        normalized = normalized.toLowerCase(java.util.Locale.ROOT);

17-28: 트랜잭션 경계 점검

클래스/메서드에 @transactional이 없는데, touch/save가 동일 트랜잭션 내에서 수행되어야 일관된 수정 시간이 보장됩니다. 상위 서비스에서 트랜잭션을 여는지 확인 부탁드립니다(포트 구현에서 처리 중이면 무관).

원하시면 호출 경로 점검 스크립트/간단한 @transactional 추가 PR 패치 드리겠습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira 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 c987296 and 0597611.

📒 Files selected for processing (1)
  • src/main/java/konkuk/thip/recentSearch/application/service/manager/RecentSearchCreateManager.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 (2)
src/main/java/konkuk/thip/recentSearch/application/service/manager/RecentSearchCreateManager.java (2)

23-27: Java 최소 버전 확인(Optional#ifPresentOrElse)

ifPresentOrElse는 Java 9+ 기능입니다. 프로젝트의 target JDK가 8이라면 기존 if/else 형태로 변경 필요합니다.

JDK 8 대응안:

var existing = recentSearchQueryPort.findRecentSearchByKeywordAndUserId(normalized, userId, type);
if (existing.isPresent()) {
    recentSearchCommandPort.touch(existing.get());
} else {
    recentSearchCommandPort.save(RecentSearch.withoutId(normalized, type, userId));
}

18-18: isFinalized 가드 도입 적합 — 호출부가 '완료'만 true로 전달하는지 확인 필요
호출부의 4번째 인자(userSearchQuery.isFinalized(), isFinalized, query.isFinalized())가 실제로 '검색 완료' 이벤트에서만 true인지 검증할 것. 확인 대상: src/main/java/konkuk/thip/user/application/service/UserSearchService.java:32, src/main/java/konkuk/thip/book/application/service/BookSearchService.java:71, src/main/java/konkuk/thip/room/application/service/RoomSearchService.java:52-54.

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.

수정해주신것 확인했습니다!

Comment on lines +19 to +20
if (keyword == null || keyword.trim().isEmpty()) return;
String normalized = keyword.trim();
Copy link
Collaborator

Choose a reason for hiding this comment

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

확인했습니다

@hd0rable hd0rable merged commit 73d9504 into develop Sep 16, 2025
4 checks passed
@hd0rable hd0rable deleted the refactor/#302-room-improve branch September 16, 2025 13:43
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-346] [refactor] 모임방 사용성 개선 작업 및 만료된 방 조회 api 추가

3 participants