Skip to content

feat: 사용자 목록 조회 구현#232

Merged
shong9124 merged 16 commits into
devfrom
feature/3-admin-user-list
Jul 1, 2026
Merged

feat: 사용자 목록 조회 구현#232
shong9124 merged 16 commits into
devfrom
feature/3-admin-user-list

Conversation

@shong9124

@shong9124 shong9124 commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

관련 이슈

작업 내용

  • 어드민 권한으로 사용자 목록 조회 구현
    • 어드민이 아닌 일반 사용자는 목록 조회 불가능

변경 사항

  • csrf 토큰을 추가하도록 필터를 추가 구현했습니다

테스트 내용

  • 테스트 코드 작성
  • 로컬 테스트 완료
  • API 테스트 완료
  • 기타 테스트 완료

체크리스트

  • 코드 컨벤션을 지켰습니다.
  • 불필요한 코드를 제거했습니다.
  • 주석이 필요한 부분에 추가했습니다.
  • 관련 문서를 수정했습니다.
  • 리뷰어가 이해하기 쉽도록 작성했습니다.

리뷰 포인트

  • 중점적으로 봐줬으면 하는 부분을 작성해주세요.

스크린샷 / 참고 자료

  • 필요한 경우 첨부해주세요.

🔗 관련 이슈

  • close #3

📝 작업 내용

관리자가 전체 사용자 목록을 조회할 수 있어야 했고, 일반 사용자는 동일 기능에 접근할 수 없어야 했습니다. 또한 목록 조회에는 단순 조회가 아니라 **커서 기반 페이지네이션(다음 페이지 이어보기)**과 정렬/필터 조건을 API 계약에 포함해야 하므로, 컨트롤러에서 바로 데이터를 긁는 방식 대신 “요청 DTO → 서비스 → QueryDSL”로 책임을 정리하고 응답을 CursorResponse로 표준화할 필요가 있었습니다.
마지막으로 SPA 환경에서 CSRF 토큰이 올바르게 쿠키로 기록되도록 CSRF 토큰 요청/해석 흐름을 조정하고, 인증/권한/쿠키 관련 테스트를 함께 보강했습니다.

주요 변경사항

  1. 사용자 목록 조회 API 구현(어드민 전용) 및 API 계약 표준화

    • GET /api/users 엔드포인트를 UserController, UserApi에 추가하고 응답을 CursorResponse<UserResponse>로 고정했습니다.
    • 조회 조건은 UserCursorRequest로 일원화해(필터: emailLike, roleEqual, isLocked + 커서/페이징: cursor, idAfter, limit + 정렬: sortBy, sortDirection) 컨트롤러→서비스로 그대로 전달되도록 했습니다.
    • 보안적으로는 SecurityConfig에서 /api/users 권한 규칙을 메서드/경로 기준으로 재구성하여 GET /api/usersADMIN 역할만 허용하도록 했습니다.
  2. 커서 기반 페이지네이션(정렬/커서 검증/다음 커서 산출) 로직 추가

    • UserService.findUsers에서 limit + 1로 다음 페이지 존재(hasNext)를 판단하고, 실제 응답 데이터는 limit만 담아 UserMapper.toCursor(...)nextCursor/nextIdAfter/hasNext/totalCount 및 정렬 메타(sortBy, sortDirection)를 함께 반환하도록 했습니다.
    • 정렬 키는 UserSortBy로 열거화했고(문자열 입력은 대소문자 무시), 잘못된 값은 InvalidUserSortByException(400)으로 실패하도록 했습니다.
    • 커서 조건의 유효성/파싱 오류는 UserQueryRepositoryImpl에서 InvalidCursorException으로 처리되며, 정렬 필드에 따라 커서 비교 로직을 분기했습니다(정렬 값 동일 시 보조키인 user.id로 누락/중복 위험을 줄이도록 구성한 트레이드오프: 비교 로직이 복잡해지지만 이어보기 정확도를 우선).
  3. SPA CSRF 토큰 발급 흐름 보정 및 관련 테스트 안정화

    • SecurityConfig의 CSRF 토큰 요청 핸들러를 CsrfTokenRequestAttributeHandler에서 SpaCsrfTokenRequestHandler로 교체해, 렌더링 단계에서 csrfToken.get()을 강제로 로드하고 헤더의 존재 여부에 따라 plain/XOR 디코딩을 분기하도록 했습니다.
    • 리프레시 토큰(AuthController의 refresh) 관련 동작 안정성을 위해 어노테이션/쿠키 검증 방식 테스트를 보강했습니다(단일 문자열 비교 → 다중 쿠키 값 검증으로 변경, XSRF 토큰 쿠키 미발급 기대 케이스 유지).

📊 변경 효과 요약 (가능한 경우에만)

  • 변경 라인(제공된 파일별 통계 합산): +942 / -26
  • 변경 파일 수: 20개
  • 테스트 보강:
    • UserControllerIntegrationTest 사용자 목록 조회 시나리오 3개 추가(관리자 성공/일반 사용자 403/비인증 401)
    • UserControllerTest 사용자 목록 조회 동작 검증 3개 추가
    • UserRepositoryTest 커서/정렬 이어보기 검증 4개 시나리오 추가 + 잘못된 커서 예외 검증
    • UserServiceTest 목록 조회 호출 흐름(Repository/Mapper 연계) 1개 추가
  • 정량 지표는 위 파일별 변경/테스트 추가 수 외에는 측정하지 않음.

@shong9124 shong9124 self-assigned this Jun 30, 2026
@shong9124 shong9124 added the enhancement New feature or request label Jun 30, 2026
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c84ddf00-bd3d-4d43-a84e-6a2af5958f2e

📥 Commits

Reviewing files that changed from the base of the PR and between c21e48b and 7e33cc1.

📒 Files selected for processing (2)
  • src/main/java/com/codeit/team5/mopl/config/SecurityConfig.java
  • src/test/java/com/codeit/team5/mopl/user/repository/UserRepositoryTest.java

📝 Walkthrough

Walkthrough

CSRF 요청 핸들러를 SpaCsrfTokenRequestHandler로 교체하고, /api/users 인가 규칙을 메서드·경로별로 재정의했습니다. 사용자 목록 조회를 위해 UserSortBy, UserCursorRequest, InvalidUserSortByException, UserQueryRepository와 구현체를 추가했고, UserService.findUsersUserMapper.toCursor가 커서 응답을 구성합니다. UserControllerUserApiGET /api/users가 추가됐고, 관련 테스트가 컨트롤러·서비스·저장소·인증 응답까지 확장됐습니다.

Estimated code review effort: 4 (Complex) | ~60분

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning CSRF 핸들러, AuthController, 인증 테스트 변경은 #3의 사용자 목록 조회 범위를 넘어서는 별도 작업입니다. CSRF/auth 변경은 별도 PR로 분리하거나, 목록 조회와의 직접적 필요성을 설명해 범위 내 변경임을 명확히 해주세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 사용자 목록 조회 구현이라는 핵심 변경을 정확하고 간결하게 요약합니다.
Description check ✅ Passed 필수 섹션들이 모두 채워져 있고 테스트와 체크리스트도 포함되어 템플릿을 충분히 따릅니다.
Linked Issues check ✅ Passed 관리자 사용자 목록 조회, 커서 페이지네이션, 권한 검증, 응답 DTO가 모두 구현되어 #3의 요구를 충족합니다.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/3-admin-user-list

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/com/codeit/team5/mopl/auth/controller/api/AuthApi.java`:
- Around line 59-60: The `CsrfToken` parameter in `AuthApi.login`/`refresh` is
being exposed in OpenAPI as if clients must send it, so hide it from the
generated docs or remove it if it is not needed in the controller logic. Update
the `CsrfToken`-related method signatures in `AuthApi` to either annotate the
parameter with hidden OpenAPI metadata (same approach as `csrfToken()`) or
eliminate the parameter entirely where it is unused, keeping only the request
fields that clients actually provide.

In `@src/main/java/com/codeit/team5/mopl/auth/controller/AuthController.java`:
- Around line 46-52: Move XSRF-TOKEN cookie creation out of AuthController so
/sign-in and /refresh no longer build ResponseCookie directly. Let
CookieCsrfTokenRepository handle issuing the CSRF cookie, and keep any
secure/SameSite/Max-Age settings centralized there or in its customizer so
SecurityConfig and the controller stay consistent. Use AuthController, signIn,
and refresh as the places to remove the duplicated cookie logic.

In `@src/main/java/com/codeit/team5/mopl/auth/filter/CsrfCookieFilter.java`:
- Around line 12-13: `CsrfCookieFilter` is being registered twice, once as a
Spring component and again in `SecurityConfig` via `addFilterAfter(...,
CsrfFilter.class)`, which can cause the `OncePerRequestFilter` logic to be
skipped in the security chain. Remove the `@Component` from `CsrfCookieFilter`
and keep it managed only through the security configuration, or alternatively
disable container auto-registration with a
`FilterRegistrationBean<CsrfCookieFilter>` so it runs only in the intended
chain.

In `@src/main/java/com/codeit/team5/mopl/user/controller/api/UserApi.java`:
- Around line 146-148: `getUsers`만 OpenAPI 문서 메타데이터가 빠져 있으니 `UserApi`의 다른 메서드들처럼
`@Operation`과 `@ApiResponses`를 추가해 요약, 어드민 전용 권한, 200/401/403 응답을 문서화하세요;
`getUsers` 시그니처의 `@Parameter(hidden = true)`와 `UserCursorRequest` 사이 공백도 정리해
가독성을 맞추고, `registerUser`, `getUser`, `updateRole`의 패턴을 그대로 따르세요.

In `@src/main/java/com/codeit/team5/mopl/user/dto/request/UserCursorRequest.java`:
- Around line 17-19: `UserCursorRequest`의 `limit`에 상한 검증이 없어 과도한 조회가 가능합니다.
`limit` 필드에 적절한 최대값 정책을 정해 `@Max`를 추가해 입력 단계에서 차단되도록 수정하세요. `UserCursorRequest`와
이를 사용하는 `UserService.findUsers`를 기준으로 위치를 찾아, 현재 `@NotNull `@Positive`` 조합에 상한 검증을
보강하면 됩니다.

In
`@src/main/java/com/codeit/team5/mopl/user/repository/querydsl/UserQueryRepositoryImpl.java`:
- Around line 99-115: In UserQueryRepositoryImpl, the CREATED_AT and LOCKED
cursor parsing paths can leak invalid client input as a 500 or silently
mis-handle it. Update the cursor handling inside the CREATED_AT and LOCKED
branches to validate/parse the external cursor safely, and convert malformed
values into a BAD_REQUEST domain exception such as InvalidUserSortByException
instead of letting Instant.parse or Boolean.parseBoolean drive the behavior.
Keep the fix localized around the existing query builder logic in
UserQueryRepositoryImpl so the pagination filter still uses user.createdAt,
user.locked, and user.id consistently after validation.

In
`@src/test/java/com/codeit/team5/mopl/auth/controller/AuthControllerIntegrationTest.java`:
- Around line 69-76: 통합 테스트에서 REFRESH_TOKEN만 검증하고 있어 CsrfCookieFilter와
CookieCsrfTokenRepository가 함께 XSRF-TOKEN을 발급하는지 놓칠 수 있습니다.
AuthControllerIntegrationTest의 해당 응답 검증에 XSRF-TOKEN Set-Cookie 확인을 추가하고,
REFRESH_TOKEN 검증과 함께 실제 보안 필터 체인을 통해 CSRF 쿠키가 내려오는지 명시적으로 검증하도록 수정하세요.

In `@src/test/java/com/codeit/team5/mopl/auth/controller/AuthControllerTest.java`:
- Around line 122-129: The AuthControllerTest assertion currently verifies only
the REFRESH_TOKEN Set-Cookie, so it can miss regressions in the new XSRF-TOKEN
contract. Update the login/refresh response checks in AuthControllerTest to also
assert that a Set-Cookie header for XSRF-TOKEN is present, and if possible
validate its expected attributes alongside the existing cookie checks. Use the
existing request/response assertion block around the REFRESH_TOKEN header
matchers as the place to add the new XSRF-TOKEN verification so both cookies are
covered.

In `@src/test/java/com/codeit/team5/mopl/user/repository/UserRepositoryTest.java`:
- Around line 144-179: The current test covers filtering and sorting in
UserRepositoryTest.findUsers_filterByRoleAndLocked_success, but it never
exercises the cursor-pagination path because idAfter/cursor are null. Update
this test or add a companion case to pass a real nextCursor/nextIdAfter from an
initial page into a second request, and verify the second page continues without
duplicates or omissions. Use UserRepository.findUsers and countUsers with a
cursor-filled UserCursorRequest so applyCursor and its switch branches are
actually covered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 90913fb7-5e1a-4b36-8cdd-74360ace2cd6

📥 Commits

Reviewing files that changed from the base of the PR and between a75e081 and d281237.

📒 Files selected for processing (21)
  • src/main/java/com/codeit/team5/mopl/auth/controller/AuthController.java
  • src/main/java/com/codeit/team5/mopl/auth/controller/api/AuthApi.java
  • src/main/java/com/codeit/team5/mopl/auth/filter/CsrfCookieFilter.java
  • src/main/java/com/codeit/team5/mopl/config/SecurityConfig.java
  • src/main/java/com/codeit/team5/mopl/config/WebConfig.java
  • src/main/java/com/codeit/team5/mopl/user/constant/UserSortBy.java
  • src/main/java/com/codeit/team5/mopl/user/controller/UserController.java
  • src/main/java/com/codeit/team5/mopl/user/controller/api/UserApi.java
  • src/main/java/com/codeit/team5/mopl/user/dto/request/UserCursorRequest.java
  • src/main/java/com/codeit/team5/mopl/user/exception/InvalidUserSortByException.java
  • src/main/java/com/codeit/team5/mopl/user/mapper/UserMapper.java
  • src/main/java/com/codeit/team5/mopl/user/repository/UserRepository.java
  • src/main/java/com/codeit/team5/mopl/user/repository/querydsl/UserQueryRepository.java
  • src/main/java/com/codeit/team5/mopl/user/repository/querydsl/UserQueryRepositoryImpl.java
  • src/main/java/com/codeit/team5/mopl/user/service/UserService.java
  • src/test/java/com/codeit/team5/mopl/auth/controller/AuthControllerIntegrationTest.java
  • src/test/java/com/codeit/team5/mopl/auth/controller/AuthControllerTest.java
  • src/test/java/com/codeit/team5/mopl/user/controller/UserControllerIntegrationTest.java
  • src/test/java/com/codeit/team5/mopl/user/controller/UserControllerTest.java
  • src/test/java/com/codeit/team5/mopl/user/repository/UserRepositoryTest.java
  • src/test/java/com/codeit/team5/mopl/user/service/UserServiceTest.java

Comment thread src/main/java/com/codeit/team5/mopl/auth/controller/api/AuthApi.java Outdated
Comment thread src/main/java/com/codeit/team5/mopl/auth/controller/AuthController.java Outdated
Comment thread src/main/java/com/codeit/team5/mopl/auth/filter/CsrfCookieFilter.java Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
src/test/java/com/codeit/team5/mopl/auth/controller/AuthControllerIntegrationTest.java (1)

63-82: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

새 SPA CSRF 플로우를 실제로 검증하는 통합 테스트가 필요합니다.

지금 성공 케이스는 with(csrf())로 테스트 전용 토큰을 주입해서 통과시키고 있어서, 이번 PR의 핵심인 SpaCsrfTokenRequestHandler.resolveCsrfTokenValue()의 plain-header 분기와 /api/auth/csrf-token로 받은 XSRF-TOKEN 값을 X-XSRF-TOKEN 헤더로 다시 보내는 경로를 전혀 타지 않습니다. 이 상태에서는 헤더 해석이 깨져도 스위트가 계속 녹색일 수 있고, 반대로 XSRF-TOKEN 부재를 계약처럼 고정하면 향후 토큰 회전 같은 합법적인 보안 변경도 테스트가 막게 됩니다. /api/auth/csrf-token 호출 → 응답 쿠키 추출 → 같은 값으로 보호된 POST 호출까지 이어지는 시나리오를 하나 추가해 두는 편이 안전합니다.

Also applies to: 212-227

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/com/codeit/team5/mopl/auth/controller/AuthControllerIntegrationTest.java`
around lines 63 - 82, The current auth integration test only passes because it
injects a test CSRF token with with(csrf()), so it never exercises the new SPA
CSRF flow. Update AuthControllerIntegrationTest to add a real end-to-end
scenario that calls /api/auth/csrf-token, extracts the XSRF-TOKEN cookie, and
then uses that same value as the X-XSRF-TOKEN header on the protected sign-in
request, verifying SpaCsrfTokenRequestHandler.resolveCsrfTokenValue() and the
plain-header path. Keep the existing login assertions in the same test area, but
do not hardcode XSRF-TOKEN absence as a contract for all cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/com/codeit/team5/mopl/user/repository/querydsl/UserQueryRepositoryImpl.java`:
- Around line 82-84: The cursor pagination guard in UserQueryRepositoryImpl
currently falls back to the first page when only one of request.cursor() or
request.idAfter() is present, which hides invalid next-page requests. Update the
validation around the existing null/blank check so that request.cursor() and
request.idAfter() must be both present or both absent, and throw
InvalidCursorException when only one is supplied. If you prefer a broader fix,
add cross-field validation at the DTO level, but keep the repository logic
aligned with the same contract.

In `@src/test/java/com/codeit/team5/mopl/user/repository/UserRepositoryTest.java`:
- Around line 145-211: This test currently covers only NAME sorting and cursor
tie-breaking, but it does not exercise the new CREATED_AT/LOCKED cursor parsing
branch or the malformed cursor exception mapping. Add one successful repository
test and one failure test around UserRepositoryTest / findUsers / countUsers
that build a cursor request using the new cursor fields, verify the parsed
filtering/sorting behavior, and assert that an invalid cursor is converted into
the expected exception.

---

Outside diff comments:
In
`@src/test/java/com/codeit/team5/mopl/auth/controller/AuthControllerIntegrationTest.java`:
- Around line 63-82: The current auth integration test only passes because it
injects a test CSRF token with with(csrf()), so it never exercises the new SPA
CSRF flow. Update AuthControllerIntegrationTest to add a real end-to-end
scenario that calls /api/auth/csrf-token, extracts the XSRF-TOKEN cookie, and
then uses that same value as the X-XSRF-TOKEN header on the protected sign-in
request, verifying SpaCsrfTokenRequestHandler.resolveCsrfTokenValue() and the
plain-header path. Keep the existing login assertions in the same test area, but
do not hardcode XSRF-TOKEN absence as a contract for all cases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5d34bd96-b0bd-4e4f-b8b3-672df7c5cf00

📥 Commits

Reviewing files that changed from the base of the PR and between d281237 and 866e766.

📒 Files selected for processing (10)
  • src/main/java/com/codeit/team5/mopl/auth/controller/AuthController.java
  • src/main/java/com/codeit/team5/mopl/auth/handler/SpaCsrfTokenRequestHandler.java
  • src/main/java/com/codeit/team5/mopl/config/SecurityConfig.java
  • src/main/java/com/codeit/team5/mopl/user/constant/UserSortBy.java
  • src/main/java/com/codeit/team5/mopl/user/controller/api/UserApi.java
  • src/main/java/com/codeit/team5/mopl/user/dto/request/UserCursorRequest.java
  • src/main/java/com/codeit/team5/mopl/user/repository/querydsl/UserQueryRepositoryImpl.java
  • src/test/java/com/codeit/team5/mopl/auth/controller/AuthControllerIntegrationTest.java
  • src/test/java/com/codeit/team5/mopl/auth/controller/AuthControllerTest.java
  • src/test/java/com/codeit/team5/mopl/user/repository/UserRepositoryTest.java

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (1)
src/main/java/com/codeit/team5/mopl/user/repository/querydsl/UserQueryRepositoryImpl.java (1)

111-153: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

커서 비교 로직 5개가 거의 동일한 패턴으로 반복됩니다.

nameCursorCondition, emailCursorCondition, createdAtCursorCondition, lockedCursorCondition, roleCursorCondition 모두 "정렬필드 gt/lt 또는 (eq + id 보조키 gt/lt)" 패턴을 그대로 복붙한 형태입니다. 필드 타입만 다를 뿐이라 제네릭 헬퍼로 추출하면 중복을 줄이고, 향후 정렬 기준 추가 시 실수(예: 위에서 지적한 ROLE 케이스처럼 비교 기준이 어긋나는 것)를 예방할 수 있습니다.

♻️ 제안하는 리팩터 방향
-    private BooleanExpression nameCursorCondition(String cursor, UUID idAfter, boolean isAsc) {
-        return isAsc
-                ? user.name.gt(cursor)
-                .or(user.name.eq(cursor).and(user.id.gt(idAfter)))
-                : user.name.lt(cursor)
-                        .or(user.name.eq(cursor).and(user.id.lt(idAfter)));
-    }
-
-    private BooleanExpression emailCursorCondition(String cursor, UUID idAfter, boolean isAsc) {
-        return isAsc
-                ? user.email.gt(cursor)
-                .or(user.email.eq(cursor).and(user.id.gt(idAfter)))
-                : user.email.lt(cursor)
-                        .or(user.email.eq(cursor).and(user.id.lt(idAfter)));
-    }
+    private <T extends Comparable<?>> BooleanExpression compareCursor(
+            ComparableExpressionBase<T> path, T value, UUID idAfter, boolean isAsc) {
+        return isAsc
+                ? path.gt(value).or(path.eq(value).and(user.id.gt(idAfter)))
+                : path.lt(value).or(path.eq(value).and(user.id.lt(idAfter)));
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/codeit/team5/mopl/user/repository/querydsl/UserQueryRepositoryImpl.java`
around lines 111 - 153, The cursor comparison logic in UserQueryRepositoryImpl
is duplicated across nameCursorCondition, emailCursorCondition,
createdAtCursorCondition, lockedCursorCondition, and roleCursorCondition.
Extract the shared “field gt/lt or (eq + id gt/lt)” pattern into a reusable
helper that accepts the field comparison expressions and the cursor parser, then
have each of those methods delegate to it. Keep the existing type-specific
cursor parsing in parseInstantCursor and parseBooleanCursor, but centralize the
ordering logic so adding new sort fields only requires one implementation path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/com/codeit/team5/mopl/user/repository/querydsl/UserQueryRepositoryImpl.java`:
- Line 92: User 조회의 커서 검증에서 notification 도메인 예외를 직접 쓰고 있으니,
UserQueryRepositoryImpl의 cursor 검증 로직과 관련 호출부에서
com.codeit.team5.mopl.notification.exception.InvalidCursorException 참조를 제거하고
user 도메인용 예외로 바꾸세요. user.exception 패키지에 InvalidCursorException을 새로 분리해
UserQueryRepositoryImpl의 검증/throw 지점과 생성자 import를 모두 해당 예외로 교체하고, 사용자 조회에 맞는
메시지나 처리 흐름도 함께 정리하세요.

---

Outside diff comments:
In
`@src/main/java/com/codeit/team5/mopl/user/repository/querydsl/UserQueryRepositoryImpl.java`:
- Around line 111-153: The cursor comparison logic in UserQueryRepositoryImpl is
duplicated across nameCursorCondition, emailCursorCondition,
createdAtCursorCondition, lockedCursorCondition, and roleCursorCondition.
Extract the shared “field gt/lt or (eq + id gt/lt)” pattern into a reusable
helper that accepts the field comparison expressions and the cursor parser, then
have each of those methods delegate to it. Keep the existing type-specific
cursor parsing in parseInstantCursor and parseBooleanCursor, but centralize the
ordering logic so adding new sort fields only requires one implementation path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 89ab02b7-0005-4f2f-8696-3891d1a9e304

📥 Commits

Reviewing files that changed from the base of the PR and between 866e766 and 9a9bbbb.

📒 Files selected for processing (2)
  • src/main/java/com/codeit/team5/mopl/user/repository/querydsl/UserQueryRepositoryImpl.java
  • src/test/java/com/codeit/team5/mopl/user/repository/UserRepositoryTest.java

@shong9124 shong9124 requested a review from conradrado July 1, 2026 04:34
@shong9124 shong9124 force-pushed the feature/3-admin-user-list branch from 9a9bbbb to c21e48b Compare July 1, 2026 04:46

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (1)
src/main/java/com/codeit/team5/mopl/user/controller/api/UserApi.java (1)

145-164: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

@Operation/@ApiResponses 추가는 좋지만, 두 가지 더 다듬을 부분이 있어요.

  1. (기존에 지적됐던 부분) Line 163 @Parameter(hidden = true)UserCursorRequest request — 애너테이션과 타입 사이 공백이 여전히 빠져 있습니다. @Parameter(hidden = true) UserCursorRequest request로 정리해주세요.

  2. (신규) hidden = true로 처리하면 UserCursorRequest에 들어있는 emailLike, roleEqual, isLocked, cursor, cursorId, limit, sortBy, sortDirection 필드가 Swagger 문서에서 전부 사라집니다. 목록/필터/정렬 옵션이 많은 이 엔드포인트일수록 소비자가 어떤 쿼리 파라미터를 넣을 수 있는지 문서에서 확인하지 못하게 되는 건 꽤 아쉬운 손해입니다.

    • 대안 A: hidden = true를 유지하고 각 필드마다 별도 @Parameter를 명시 — 설명 문구를 세밀하게 통제할 수 있지만 보일러플레이트가 늘어남.
    • 대안 B: springdoc-openapi가 제공하는 @ParameterObject(패키지: org.springdoc.core.annotations.ParameterObject, v2.x 기준)로 교체 — 복합 객체의 필드를 자동으로 개별 쿼리 파라미터로 펼쳐서 문서화해줍니다. 보일러플레이트는 적지만 설명 문구는 UserCursorRequest 필드 쪼의 애너테이션에 의존하게 됩니다.
🛠️ 제안 diff
-    ResponseEntity<CursorResponse<UserResponse>> getUsers(
-            `@Parameter`(hidden = true)UserCursorRequest request
-    );
+    ResponseEntity<CursorResponse<UserResponse>> getUsers(
+            `@org.springdoc.core.annotations.ParameterObject` UserCursorRequest request
+    );

springdoc 버전별로 패키지 경로가 다를 수 있으니(과거 1.x는 org.springdoc.api.annotations.ParameterObject), 현재 프로젝트가 사용하는 2.8.17 기준으로 정확한 경로를 한 번 확인해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/codeit/team5/mopl/user/controller/api/UserApi.java` around
lines 145 - 164, The `getUsers` API method still has the missing space in
`@Parameter(hidden = true) UserCursorRequest request`, and hiding the whole
request also removes all query/filter/sort fields from Swagger. Fix the spacing
first, then replace the hidden request approach in `UserApi.getUsers` with
`@ParameterObject` for `UserCursorRequest` (using the project’s springdoc v2.x
annotation package) so `emailLike`, `roleEqual`, `isLocked`, `cursor`,
`cursorId`, `limit`, `sortBy`, and `sortDirection` are documented as query
parameters. If you need custom descriptions, add them on the corresponding
`UserCursorRequest` fields instead of hiding the object.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/com/codeit/team5/mopl/config/SecurityConfig.java`:
- Around line 55-71: The SecurityConfig authorization rules for /api/users are
too granular without a fallback, so any new or missed /api/users method can fall
through to anyRequest().permitAll(). Add a catch-all authenticated rule for the
/api/users path family in the same requestMatchers chain, keeping the explicit
POST/GET/PATCH rules for existing cases while ensuring unhandled /api/users/**
requests in SecurityConfig do not become publicly accessible.

In `@src/main/java/com/codeit/team5/mopl/user/constant/UserSortBy.java`:
- Around line 11-15: `UserSortBy`의 `LOCKED` 매핑만 다른 enum 상수들과 달리 `"isLocked"`
접두사를 쓰고 있어 네이밍 패턴이 불일치합니다. `UserSortBy` enum에서 `NAME`, `EMAIL`, `CREATED_AT`,
`ROLE`과 같은 기준으로 `LOCKED`의 실제 정렬 필드명이 무엇인지 확인하고, 엔티티의 필드/getter 이름과 맞게 매핑값을 통일하거나
필요하면 의도를 드러내는 주석을 추가해 혼동을 줄이세요.

In
`@src/main/java/com/codeit/team5/mopl/user/repository/querydsl/UserQueryRepositoryImpl.java`:
- Around line 155-161: The parseInstantCursor method is swallowing the original
DateTimeParseException, so preserve the stack trace by passing the caught
exception as the cause when throwing InvalidCursorException. Update the
exception handling in UserQueryRepositoryImpl.parseInstantCursor to use the
constructor that accepts Throwable cause if available, or add one to
InvalidCursorException so the original parsing failure details are retained for
debugging.

In `@src/test/java/com/codeit/team5/mopl/user/repository/UserRepositoryTest.java`:
- Around line 146-376: Add a missing ROLE cursor pagination test in
UserRepositoryTest to cover the UserSortBy.ROLE path that is already handled by
UserQueryRepositoryImpl. Create a findUsers_roleCursor_success case alongside
the existing CREATED_AT and LOCKED tests, using UserCursorRequest with
UserSortBy.ROLE and a cursor built from the last item’s role plus id. Verify the
paged results are ordered consistently, combine cleanly across pages, and
exercise the roleCursorCondition/buildOrder behavior for regression coverage.

---

Duplicate comments:
In `@src/main/java/com/codeit/team5/mopl/user/controller/api/UserApi.java`:
- Around line 145-164: The `getUsers` API method still has the missing space in
`@Parameter(hidden = true) UserCursorRequest request`, and hiding the whole
request also removes all query/filter/sort fields from Swagger. Fix the spacing
first, then replace the hidden request approach in `UserApi.getUsers` with
`@ParameterObject` for `UserCursorRequest` (using the project’s springdoc v2.x
annotation package) so `emailLike`, `roleEqual`, `isLocked`, `cursor`,
`cursorId`, `limit`, `sortBy`, and `sortDirection` are documented as query
parameters. If you need custom descriptions, add them on the corresponding
`UserCursorRequest` fields instead of hiding the object.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 565d2269-453c-4bb7-9df6-d2ad6a1a3937

📥 Commits

Reviewing files that changed from the base of the PR and between 9a9bbbb and c21e48b.

📒 Files selected for processing (20)
  • src/main/java/com/codeit/team5/mopl/auth/controller/AuthController.java
  • src/main/java/com/codeit/team5/mopl/auth/handler/SpaCsrfTokenRequestHandler.java
  • src/main/java/com/codeit/team5/mopl/config/SecurityConfig.java
  • src/main/java/com/codeit/team5/mopl/config/WebConfig.java
  • src/main/java/com/codeit/team5/mopl/user/constant/UserSortBy.java
  • src/main/java/com/codeit/team5/mopl/user/controller/UserController.java
  • src/main/java/com/codeit/team5/mopl/user/controller/api/UserApi.java
  • src/main/java/com/codeit/team5/mopl/user/dto/request/UserCursorRequest.java
  • src/main/java/com/codeit/team5/mopl/user/exception/InvalidUserSortByException.java
  • src/main/java/com/codeit/team5/mopl/user/mapper/UserMapper.java
  • src/main/java/com/codeit/team5/mopl/user/repository/UserRepository.java
  • src/main/java/com/codeit/team5/mopl/user/repository/querydsl/UserQueryRepository.java
  • src/main/java/com/codeit/team5/mopl/user/repository/querydsl/UserQueryRepositoryImpl.java
  • src/main/java/com/codeit/team5/mopl/user/service/UserService.java
  • src/test/java/com/codeit/team5/mopl/auth/controller/AuthControllerIntegrationTest.java
  • src/test/java/com/codeit/team5/mopl/auth/controller/AuthControllerTest.java
  • src/test/java/com/codeit/team5/mopl/user/controller/UserControllerIntegrationTest.java
  • src/test/java/com/codeit/team5/mopl/user/controller/UserControllerTest.java
  • src/test/java/com/codeit/team5/mopl/user/repository/UserRepositoryTest.java
  • src/test/java/com/codeit/team5/mopl/user/service/UserServiceTest.java

Comment thread src/main/java/com/codeit/team5/mopl/config/SecurityConfig.java
Comment thread src/main/java/com/codeit/team5/mopl/user/constant/UserSortBy.java
@shong9124 shong9124 removed the request for review from conradrado July 1, 2026 05:09
@shong9124 shong9124 requested a review from conradrado July 1, 2026 05:29

@conradrado conradrado left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

고생하셨습니다. 승인드립니다!

@shong9124 shong9124 merged commit cec8bda into dev Jul 1, 2026
2 checks passed
@shong9124 shong9124 deleted the feature/3-admin-user-list branch July 1, 2026 05:50
@coderabbitai coderabbitai Bot mentioned this pull request Jul 3, 2026
9 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 사용자 관리 - 사용자 목록 조회

2 participants