Skip to content

[WTH-458] 어드민 멤버 관리 페이지 API 개편 - #96

Merged
hyxklee merged 2 commits into
devfrom
refactor/WTH-458-마이페이지-개편에-따른-백엔드-수정
Jul 31, 2026

Hidden character warning

The head ref may contain hidden characters: "refactor/WTH-458-\ub9c8\uc774\ud398\uc774\uc9c0-\uac1c\ud3b8\uc5d0-\ub530\ub978-\ubc31\uc5d4\ub4dc-\uc218\uc815"
Merged

[WTH-458] 어드민 멤버 관리 페이지 API 개편#96
hyxklee merged 2 commits into
devfrom
refactor/WTH-458-마이페이지-개편에-따른-백엔드-수정

Conversation

@hyxklee

@hyxklee hyxklee commented Jul 30, 2026

Copy link
Copy Markdown
Member

개요

어드민 멤버 관리 페이지 개편에 맞춰 멤버 목록 API를 페이지네이션·검색·정렬 기반으로 전환하고, 상세 모달용 단건 조회를 추가했습니다.

기존 GET /admin/clubs/{clubId}/members는 파라미터 없이 전체 멤버를 List로 반환하고 있어 새 화면 요구사항(페이지네이션, 기수 탭, 검색, 정렬)을 담을 수 없었습니다.

변경 사항

1. 멤버 목록 — 페이지네이션·필터·검색·정렬

GET /api/v4/admin/clubs/{clubId}/members
    ?page=0&size=20
    &keyword={이름|학과|학번}
    &cardinalNumber={기수}
    &sort=CARDINAL_DESC
  • 응답이 CommonResponse<List<ClubMemberResponse>>CommonResponse<PageResponse<ClubMemberResponse>> 로 변경됩니다 (프론트 대응 필요)
  • sort: CARDINAL_DESC(기본) / CARDINAL_ASC / NAME_ASC / JOINED_DESC
  • 가입 대기·추방·탈퇴 멤버도 포함 (관리 화면이므로 기존 동작 유지)

2. 관리자용 멤버 상세 조회

GET /api/v4/admin/clubs/{clubId}/members/{clubMemberId}

상세 모달용. 일반 사용자용 findMyMemberProfile과 달리 WAITING/BANNED/LEFT 멤버도 조회됩니다. 목록 행과 동일한 ClubMemberResponse를 재사용합니다(account 도메인의 AccountTransactionResponse와 같은 방식).

3. 응답 필드 추가

profileImageUrl, bio, joinedAt

계획서에는 ClubMember.profileImageStorageKey / bio를 쓰라고 되어 있었지만, 멀티프로필(WTH-433/451/454) 도입 이후 이 두 필드에 쓰는 코드가 없어 항상 null입니다. 현재 프로필 소유자는 ClubMember.userProfile이므로 userProfile 우선 + 레거시 필드 fallback으로 구현했습니다.

구현 노트

기수 정렬을 Spring Sort로 표현할 수 없습니다. 기수는 ClubMemberCardinal 별도 엔티티(멤버 1:N)라 프로퍼티 매핑이 안 됩니다. 그래서 ClubMemberSort는 다른 정렬 enum과 달리 toSort()를 두지 않고, JPQL ORDER BY에서 최대 기수번호 스칼라 서브쿼리로 처리합니다. 정렬 키는 문자열 연결이 아니라 CASE WHEN :sortKey = '...' 비교라 정렬 키 인젝션 여지가 없습니다.

페이지 경계 안정성: 모든 정렬 분기 끝에 cm.id ASC 타이브레이커를 둬서 같은 기수 멤버가 페이지 경계에서 중복/누락되지 않습니다.

N+1 회피: 기수 목록은 조회된 페이지의 멤버에 대해서만 일괄 조회합니다. count 쿼리와 본 쿼리의 WHERE 조건은 완전히 동일하고, fetch join은 ToOne(user, userProfile)뿐이라 페이징이 인메모리로 떨어지지 않습니다.

테스트

  • ClubMemberAdminQueryTest (신규, @DataJpaTest + Testcontainers) — 정렬 4종의 순서까지 실DB로 단언, 기수 필터·keyword 검색·동시 적용, 페이지 경계 중복/누락 없음, 상태 무관 상세 조회. ORDER BY에 스칼라 서브쿼리를 쓰므로 mock이 아니라 실제 MySQL로 검증했습니다.
  • GetClubMemberQueryServiceTest — 페이지 응답 조립, keyword 공백 trim, size 1~100 보정, 상세 조회 실패 경로(미존재/타 동아리)

./gradlew ktlintCheck test 통과.

API 코드

코드 내용
11123 MEMBER_FIND_DETAIL_SUCCESS (신규)

이번 PR에서 제외한 것

  • 다중 선택(체크박스) 벌크 액션 — 승인·추방·권한 변경은 멤버 간 불변식이 없어 프론트의 단건 반복 호출이 더 적합하다고 판단했습니다(이미 승인된 멤버 하나 때문에 전체가 실패하는 문제). 정합성이 걸린 기수 일괄 변경만 별도 검토합니다.
  • 기수 변경의 스냅샷 레이스 — MySQL REPEATABLE READ에서 권한 검증 조회가 read view를 먼저 확정해, 비관적 락을 기다린 트랜잭션이 앞선 커밋을 보지 못하고 기수를 중복 INSERT 합니다(unique 제약이 막아 500). 기존 단건 updateCardinals·applyOb에도 있는 문제라 별도 이슈로 분리합니다.
  • 멤버 상태(memberStatus) 필터 파라미터 — 승인 대기 멤버를 찾기 위해 후속으로 필요할 수 있습니다.
  • 비밀번호 초기화 — OAuth 기반이라 비밀번호 저장 모델이 없습니다.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WwXeZkkPXJvbhj4UVBs1nU

Summary by CodeRabbit

  • 새 기능

    • 관리자 멤버 목록에 페이지네이션, 이름·학과·학번 검색, 기수 필터, 이름·가입일·기수별 정렬을 지원합니다.
    • 승인 대기·추방·탈퇴 멤버도 관리자 목록과 상세 화면에서 조회할 수 있습니다.
    • 멤버 목록에 프로필 이미지, 자기소개, 가입일 정보가 표시됩니다.
    • 멤버 상세 조회 성공 응답이 추가되었습니다.
  • 개선

    • 검색어 공백을 정리하고 페이지 크기를 최대 100개로 제한합니다.
    • 프로필 정보가 없으면 기존 멤버 정보로 대체해 표시합니다.
  • 테스트

    • 필터, 정렬, 페이지 경계 및 상태별 조회 검증을 강화했습니다.

- GET /admin/clubs/{clubId}/members를 PageResponse로 전환하고
  기수 필터·이름/학과/학번 검색·정렬(ClubMemberSort) 지원
- 기수 정렬은 ClubMemberCardinal의 최대 기수번호 기준이라
  Sort 프로퍼티로 표현할 수 없어 JPQL ORDER BY에서 처리
- 모든 정렬에 clubMemberId ASC 타이브레이커를 둬 페이지 경계 안정화
- 관리자용 멤버 상세 조회 추가 (WAITING/BANNED/LEFT 포함)
- 목록·상세 응답에 profileImageUrl, bio, joinedAt 추가
  (멀티프로필 도입 이후 userProfile 우선, 레거시 필드 fallback)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WwXeZkkPXJvbhj4UVBs1nU
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fcddbd7e-d84b-4eff-b247-2fff962fce1a

📥 Commits

Reviewing files that changed from the base of the PR and between a484e3a and 7bdc846.

📒 Files selected for processing (2)
  • src/main/kotlin/com/weeth/domain/club/presentation/ClubAdminController.kt
  • src/test/kotlin/com/weeth/domain/club/domain/repository/ClubMemberAdminQueryTest.kt

📝 Walkthrough

Walkthrough

관리자 동아리 멤버 조회가 페이지네이션, 검색, 기수 필터, 정렬을 지원하도록 변경되었습니다. 응답에 프로필 정보와 가입일이 추가되었으며, 상태와 무관한 멤버 상세 조회와 관련 테스트가 확장되었습니다.

Changes

관리자 멤버 조회

Layer / File(s) Summary
응답 및 조회 계약
src/main/kotlin/com/weeth/domain/club/application/dto/..., src/main/kotlin/com/weeth/domain/club/domain/repository/ClubMemberReader.kt
ClubMemberSort 정렬 옵션과 profileImageUrl, bio, joinedAt 응답 필드가 추가되고 관리자 목록·상세 조회 인터페이스가 정의되었습니다.
관리자 저장소 쿼리
src/main/kotlin/com/weeth/domain/club/domain/repository/ClubMemberRepository.kt
기수·키워드 필터, 기수·이름·가입일 정렬, 페이징 count 쿼리와 사용자 프로필을 함께 조회하는 상세 쿼리가 추가되었습니다.
서비스 응답 매핑 및 API 연결
src/main/kotlin/com/weeth/domain/club/application/..., src/main/kotlin/com/weeth/domain/club/presentation/...
관리자 목록 API가 페이지 응답으로 변경되고 입력값 보정, 페이지 단위 기수 조회, 멀티프로필 우선 매핑, 상태 무관 상세 조회가 연결되었습니다.
관리자 조회 검증
src/test/kotlin/com/weeth/domain/club/...
서비스 모킹 테스트와 실제 JPA 테스트가 정렬·필터·페이지 경계·상태 무관 상세 조회를 검증합니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant 관리자
  participant ClubAdminController
  participant GetClubMemberQueryService
  participant ClubMemberRepository
  participant ClubMapper
  관리자->>ClubAdminController: 페이지·검색·기수·정렬 요청
  ClubAdminController->>GetClubMemberQueryService: findClubMembersForAdmin(...)
  GetClubMemberQueryService->>ClubMemberRepository: 필터·정렬·페이지 조회
  ClubMemberRepository-->>GetClubMemberQueryService: Page<ClubMember>
  GetClubMemberQueryService->>ClubMapper: 멤버와 기수 매핑
  ClubMapper-->>GetClubMemberQueryService: ClubMemberResponse
  GetClubMemberQueryService-->>ClubAdminController: PageResponse<ClubMemberResponse>
  ClubAdminController-->>관리자: 페이지 멤버 응답
Loading

Possibly related PRs

Suggested labels: ✨ Feature, 📬 API

Suggested reviewers: dalzzy

Poem

당근 먹은 토끼가 페이지를 넘겨요
이름과 기수를 가지런히 세워요
새 프로필과 가입일을 담고
모든 상태의 멤버를 찾아요
깡총, 관리자 목록 완성이에요!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 어드민 멤버 관리 API의 페이지네이션·검색·정렬·상세 조회 개편이라는 주요 변경사항을 간결하게 설명합니다.
Description check ✅ Passed 변경 목적, 주요 변경사항, 구현 방식, 테스트 결과와 제외 범위를 충분히 설명해 템플릿의 핵심 요구사항을 충족합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/WTH-458-마이페이지-개편에-따른-백엔드-수정

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.

@hyxklee
hyxklee requested review from dalzzy and soo0711 July 30, 2026 05:35

@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

🧹 Nitpick comments (1)
src/test/kotlin/com/weeth/domain/club/domain/repository/ClubMemberAdminQueryTest.kt (1)

44-79: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

LEFT 상태 멤버 포함 여부에 대한 테스트 커버리지 보강 제안.

PR 목표에 WAITING, BANNED, LEFT 상태 멤버가 모두 목록에 포함되어야 한다고 명시되어 있으나, seed()와 "가입 대기·추방 멤버도 목록에 포함된다" 테스트는 WAITING/BANNED만 검증하고 LEFT는 다루지 않습니다. 쿼리 자체에는 상태 필터가 없어 현재는 자연히 포함되지만, 회귀 방지 관점에서 LEFT 케이스도 시딩·검증에 추가하는 것을 권장합니다.

♻️ 제안: LEFT 상태 멤버 시딩 및 검증 추가
             save("라대기", null, null, MemberStatus.WAITING, emptyList())
             save("마추방", null, null, MemberStatus.BANNED, emptyList())
+            save("바탈퇴", null, null, MemberStatus.LEFT, emptyList())
             return club.id

그리고 "가입 대기·추방 멤버도 목록에 포함된다" 테스트의 기대 목록에 MemberStatus.LEFT를 추가합니다.

Also applies to: 150-170

🤖 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/kotlin/com/weeth/domain/club/domain/repository/ClubMemberAdminQueryTest.kt`
around lines 44 - 79, Extend the ClubMemberAdminQueryTest seed() data with a
member whose MemberStatus is LEFT, then update the “가입 대기·추방 멤버도 목록에 포함된다” test
expectations to include that member and status. Keep the existing WAITING and
BANNED fixtures and assertions unchanged.
🤖 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/test/kotlin/com/weeth/domain/club/domain/repository/ClubMemberAdminQueryTest.kt`:
- Around line 132-148: Update the JOINED_DESC test in ClubMemberAdminQueryTest
to make the createdAt tie-breaker explicit and avoid relying on insertion-order
assumptions. Assert the result using the repository’s expected secondary
ordering, such as cm.id ascending for equal createdAt values, while retaining
verification that createdAt values are sorted descending.

---

Nitpick comments:
In
`@src/test/kotlin/com/weeth/domain/club/domain/repository/ClubMemberAdminQueryTest.kt`:
- Around line 44-79: Extend the ClubMemberAdminQueryTest seed() data with a
member whose MemberStatus is LEFT, then update the “가입 대기·추방 멤버도 목록에 포함된다” test
expectations to include that member and status. Keep the existing WAITING and
BANNED fixtures and assertions unchanged.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f01771a0-0a5d-48b6-94e7-a4a18929f202

📥 Commits

Reviewing files that changed from the base of the PR and between eab363c and a484e3a.

📒 Files selected for processing (10)
  • src/main/kotlin/com/weeth/domain/club/application/dto/request/ClubMemberSort.kt
  • src/main/kotlin/com/weeth/domain/club/application/dto/response/ClubMemberResponse.kt
  • src/main/kotlin/com/weeth/domain/club/application/mapper/ClubMapper.kt
  • src/main/kotlin/com/weeth/domain/club/application/usecase/query/GetClubMemberQueryService.kt
  • src/main/kotlin/com/weeth/domain/club/domain/repository/ClubMemberReader.kt
  • src/main/kotlin/com/weeth/domain/club/domain/repository/ClubMemberRepository.kt
  • src/main/kotlin/com/weeth/domain/club/presentation/ClubAdminController.kt
  • src/main/kotlin/com/weeth/domain/club/presentation/ClubResponseCode.kt
  • src/test/kotlin/com/weeth/domain/club/application/usecase/query/GetClubMemberQueryServiceTest.kt
  • src/test/kotlin/com/weeth/domain/club/domain/repository/ClubMemberAdminQueryTest.kt

@soo0711 soo0711 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.

수고하셨습니다!!
아래 하나랑 코드래빗 피드백만 보시면 될 것 같습니다 👍

): CommonResponse<List<ClubMemberResponse>> {
val members = getClubMemberQueryService.findClubMembersForAdmin(clubId, userId)
@RequestParam(defaultValue = "0") page: Int,
@RequestParam(defaultValue = "20") size: Int,

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.

Figma 기준으로 페이지 사이즈가 10이라 size도 10으로 수정해야 할 것 같습니다!

- 멤버 목록 기본 size를 Figma 기준(10)에 맞춤
- JOINED_DESC 테스트가 createdAt 컬럼 정밀도에 의존하던 문제 해결.
  seed에서 네이티브 UPDATE로 가입일을 1분 간격으로 확정하고,
  값이 실제로 서로 다른지 먼저 단언해 정밀도가 바뀌면
  순서가 아니라 그 지점에서 실패하도록 함

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WwXeZkkPXJvbhj4UVBs1nU
@hyxklee
hyxklee merged commit ed3a118 into dev Jul 31, 2026
2 checks passed
@hyxklee
hyxklee deleted the refactor/WTH-458-마이페이지-개편에-따른-백엔드-수정 branch July 31, 2026 12:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants