[Fix] WTH-329: 어드민 멤버 페이지 QA 수정사항#81
Conversation
📝 WalkthroughWalkthrough관리자 영역을 운영진으로 재브랜딩하고, 기수 관리(UI/훅/API), 리더 권한 이양, 서버측 대시보드 조회 및 UserHydrator 기반 사용자·클럽 상태 하이드레이션을 추가했습니다. 회원 관리 UI/테이블/모달과 관련 유틸·상수도 리팩토링했습니다. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Review rate limit: 0/1 reviews remaining, refill in 60 minutes.Comment |
PR 테스트 결과✅ Jest: 통과 🎉 모든 테스트를 통과했습니다! |
🤖 Claude 테스트 제안
변경된 컴포넌트에 대해 Claude가 생성한 테스트 코드입니다. 검토 후 적합한 부분만 사용하세요.
|
|
구현한 기능 Preview: https://weeth-6qz4a84mn-weethsite-4975s-projects.vercel.app |
PR 검증 결과✅ TypeScript: 통과 🎉 모든 검증을 통과했습니다! |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
src/stores/useUserStore.ts (1)
20-20:useUserActions에도setRole를 함께 노출하는 편이 일관적입니다.새 액션이 생겼는데 selector 묶음에는 빠져 있어, 호출부 패턴이 분산될 수 있습니다.
♻️ 제안 수정안
export const useUserActions = () => useUserStore( useShallow((store) => ({ setUser: store.setUser, + setRole: store.setRole, reset: store.reset, })), );Also applies to: 31-36
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/stores/useUserStore.ts` at line 20, The selector bundle/useUserActions should include the new setRole action for consistency; add setRole (the function defined as setRole: (role: Role) => set({ role }, false, 'setRole')) to the useUserActions export alongside the other actions (the same group that includes the actions mentioned around lines 31-36) so callers can import all user actions from useUserActions rather than mixing call patterns.src/components/admin/CardinalDropdown.tsx (1)
40-40: 간소화 제안: 불필요한 화살표 함수 래퍼 제거
onSelectAll이 이미 함수이므로 직접 전달할 수 있습니다.♻️ 제안된 변경
- {onSelectAll && <DropdownMenuItem onSelect={() => onSelectAll()}>전체</DropdownMenuItem>} + {onSelectAll && <DropdownMenuItem onSelect={onSelectAll}>전체</DropdownMenuItem>}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/admin/CardinalDropdown.tsx` at line 40, The JSX uses an unnecessary arrow wrapper around the onSelectAll callback: in CardinalDropdown.tsx change the DropdownMenuItem prop from onSelect={() => onSelectAll()} to passing the function directly as onSelect={onSelectAll} to simplify and avoid creating an extra closure; locate the DropdownMenuItem usage and update similarly if any other menu items wrap callbacks in no-op arrow functions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/app/`(private)/[clubId]/admin/layout.tsx:
- Around line 18-20: The server-side call to homeServerApi.getDashboard(clubId)
in layout.tsx can throw and currently bubbles up; wrap the await call in a
try-catch inside the async component (or loader) that calls getDashboard and, on
catch, return/render a simple fallback UI (e.g., a minimal admin error
placeholder) or rethrow a controlled error after logging; alternatively add an
error boundary file named error.tsx under the same route (the [clubId]/admin
segment) to handle and display errors—target the getDashboard call and the
exported default async layout function when applying the change.
In `@src/constants/term.ts`:
- Line 61: Replace the Korean particle typos in the policy text constants:
change the occurrence of '“동아리 운영진”란' to '“동아리 운영진”이란' and change each '운영진는' to
'운영진은' wherever they appear in the term strings (the constants that contain
those sentences, e.g., the string entries shown with '“동아리 운영진”란' and the other
lines with '운영진는'); update all three occurrences so the user-facing terms are
grammatically correct before release.
In `@src/utils/shared/getApiErrorCode.ts`:
- Around line 3-4: getApiErrorCode currently returns err.response?.data?.code
without guaranteeing it's a number; update getApiErrorCode to validate/coerce
the value from isAxiosError(err) ? err.response?.data?.code so the function
truly returns number | undefined — e.g., extract the raw value from
err.response?.data?.code, check typeof === 'number' (or attempt Number(value)
and ensure !Number.isNaN), and return the numeric value or undefined; keep
references to getApiErrorCode and isAxiosError and the err.response?.data?.code
access to locate the change.
In `@src/utils/shared/runBulkMutation.ts`:
- Around line 14-23: The function currently shows toastSuccess even when args is
empty; add an initial guard in runBulkMutation (before calling
Promise.allSettled and using mutateAsync) that checks if args is falsy or
args.length === 0 and short-circuits (e.g., return early) instead of proceeding;
optionally call toastError or a specific "no items selected" message from
messages to inform the user, ensuring toastSuccess is only triggered after real
mutations complete and no errors are present (affects args, mutateAsync,
toastSuccess, toastError, resolveErrorMessage, messages).
---
Nitpick comments:
In `@src/components/admin/CardinalDropdown.tsx`:
- Line 40: The JSX uses an unnecessary arrow wrapper around the onSelectAll
callback: in CardinalDropdown.tsx change the DropdownMenuItem prop from
onSelect={() => onSelectAll()} to passing the function directly as
onSelect={onSelectAll} to simplify and avoid creating an extra closure; locate
the DropdownMenuItem usage and update similarly if any other menu items wrap
callbacks in no-op arrow functions.
In `@src/stores/useUserStore.ts`:
- Line 20: The selector bundle/useUserActions should include the new setRole
action for consistency; add setRole (the function defined as setRole: (role:
Role) => set({ role }, false, 'setRole')) to the useUserActions export alongside
the other actions (the same group that includes the actions mentioned around
lines 31-36) so callers can import all user actions from useUserActions rather
than mixing call patterns.
🪄 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
Run ID: 66d39301-2d90-41b5-a4c9-ad42a2739cd1
📒 Files selected for processing (41)
src/app/(private)/[clubId]/admin/layout.tsxsrc/app/(public)/landing/page.tsxsrc/components/admin/CardinalDropdown.tsxsrc/components/admin/board/BoardCard.tsxsrc/components/admin/board/modal/constants.tssrc/components/admin/index.tssrc/components/admin/layout/LNB.tsxsrc/components/admin/member/CardinalCard.tsxsrc/components/admin/member/CardinalPillList.tsxsrc/components/admin/member/MemberPageContent.tsxsrc/components/admin/member/MemberTable.tsxsrc/components/admin/member/MemberTopBar.tsxsrc/components/admin/member/modal/MemberDetailModal.tsxsrc/components/admin/schedule/general/SchedulePageContent.tsxsrc/components/attendance/AttendanceTodayCard.tsxsrc/components/home/HomeBoardContent.tsxsrc/components/layout/Footer.tsxsrc/components/layout/header/DefaultActions.tsxsrc/components/layout/header/Header.tsxsrc/components/layout/header/MobileNavSheet.tsxsrc/components/ui/table.tsxsrc/constants/admin/memberDetailModal.constants.tssrc/constants/admin/memberTable.constants.tssrc/constants/admin/memberTopBar.constants.tssrc/constants/errorCode.tssrc/constants/home/tutorial.tsxsrc/constants/landing/landing.tssrc/constants/term.tssrc/hooks/mutations/admin/index.tssrc/hooks/mutations/admin/useAdminCardinalMutations.tssrc/hooks/mutations/admin/useAdminMemberMutations.tssrc/lib/actions/club.tssrc/lib/apis/adminMember.tssrc/lib/apis/cardinal.tssrc/stores/useUserStore.tssrc/types/admin/member.d.tssrc/utils/admin/memberMapper.tssrc/utils/admin/parseCardinals.tssrc/utils/shared/getApiErrorCode.tssrc/utils/shared/index.tssrc/utils/shared/runBulkMutation.ts
💤 Files with no reviewable changes (1)
- src/types/admin/member.d.ts
🤖 Claude 테스트 제안
변경된 컴포넌트에 대해 Claude가 생성한 테스트 코드입니다. 검토 후 적합한 부분만 사용하세요.
|
PR 테스트 결과✅ Jest: 통과 🎉 모든 테스트를 통과했습니다! |
|
구현한 기능 Preview: https://weeth-3zd0hlzb8-weethsite-4975s-projects.vercel.app |
PR 검증 결과✅ TypeScript: 통과 🎉 모든 검증을 통과했습니다! |
There was a problem hiding this comment.
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/app/api/proxy/auth/refresh/route.ts (1)
56-96:⚠️ Potential issue | 🟠 Majorauth 리다이렉트는 요청 origin 기준으로 URL을 구성해야 합니다.
NEXT_PUBLIC_APP_URL이 현재 요청 origin과 다르면, 이 응답에서 설정한 인증 쿠키가 다른 origin으로 리다이렉트되어 접근할 수 없게 됩니다. 라인 86-94에서 쿠키를 설정한 응답을 쿠키가 유효한 origin으로 리다이렉트해야 하므로request.nextUrl.origin기준으로 URL을 만드는 것이 맞습니다.src/proxy.ts:16-24도 동일하게request.url기반으로 login URL을 구성하고 있습니다.수정안
- const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? request.nextUrl.origin; + const appUrl = request.nextUrl.origin;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/api/proxy/auth/refresh/route.ts` around lines 56 - 96, The redirect and login responses use NEXT_PUBLIC_APP_URL via the local appUrl variable which can differ from the current request origin; change constructions that use appUrl (the appUrl assignment, the buildLoginResponse(...) calls and the NextResponse.redirect(new URL(safeRedirect, appUrl)) call) to use request.nextUrl.origin (or request.url's origin) as the base so cookies and redirects are created for the current request origin instead of NEXT_PUBLIC_APP_URL; ensure all places that call buildLoginResponse or create the redirectResponse pass request.nextUrl.origin as the base URL.
🧹 Nitpick comments (1)
src/components/admin/member/MemberTable.tsx (1)
75-87: 토큰 외 값은 피해주세요.
gap-600과max-h-[600px]는 이 저장소의 TSX 스타일 가이드에서 권장하는 토큰 범위를 벗어난 hardcoded 값입니다. 기존 spacing/size 토큰으로 맞추거나, 정말 필요하다면 디자인 토큰 추가 여부를 먼저 확인해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/admin/member/MemberTable.tsx` around lines 75 - 87, The code uses hardcoded utility classes gap-600 and max-h-[600px] in MemberTable (the outer div using className and the Table wrapperClassName) which violate the TSX style guide; replace those with existing design tokens (e.g., the repository's spacing/size token classes used elsewhere such as gap-200/gap-300 or the appropriate max-h token) or, if a new token is required, coordinate with design to add it before committing; update the outer div's className and the Table wrapperClassName to use the tokenized classes instead of gap-600 and max-h-[600px].
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/admin/member/MemberTable.tsx`:
- Around line 53-54: Compute selection state against the currently visible
members instead of using selectedIds.size directly: derive a
visibleSelectedCount by counting members whose id exists in selectedIds (e.g.,
members.filter(m => selectedIds.has(m.id)).length) and then set isAllSelected =
members.length > 0 && visibleSelectedCount === members.length and hasAnySelected
= visibleSelectedCount > 0; alternatively, when members changes clear or
intersect selectedIds to keep only visible IDs. Apply the same change to the
other similar selection calculations in this file (the block around the repeated
logic).
- Around line 116-120: The TableRow currently only handles mouse clicks via
onClick and is not keyboard-accessible; update the TableRow element (the one
with key={member.id} and onClick={() => onMemberAction?.(member)}) to be
focusable and respond to Enter/Space: add tabIndex={0}, role="button" (or
appropriate ARIA role), and implement an onKeyDown handler that calls
onMemberAction?.(member) when the user presses Enter or Space (for Space,
preventDefault to avoid scrolling). Keep the existing onClick behavior and
visual cursor style.
---
Outside diff comments:
In `@src/app/api/proxy/auth/refresh/route.ts`:
- Around line 56-96: The redirect and login responses use NEXT_PUBLIC_APP_URL
via the local appUrl variable which can differ from the current request origin;
change constructions that use appUrl (the appUrl assignment, the
buildLoginResponse(...) calls and the NextResponse.redirect(new
URL(safeRedirect, appUrl)) call) to use request.nextUrl.origin (or request.url's
origin) as the base so cookies and redirects are created for the current request
origin instead of NEXT_PUBLIC_APP_URL; ensure all places that call
buildLoginResponse or create the redirectResponse pass request.nextUrl.origin as
the base URL.
---
Nitpick comments:
In `@src/components/admin/member/MemberTable.tsx`:
- Around line 75-87: The code uses hardcoded utility classes gap-600 and
max-h-[600px] in MemberTable (the outer div using className and the Table
wrapperClassName) which violate the TSX style guide; replace those with existing
design tokens (e.g., the repository's spacing/size token classes used elsewhere
such as gap-200/gap-300 or the appropriate max-h token) or, if a new token is
required, coordinate with design to add it before committing; update the outer
div's className and the Table wrapperClassName to use the tokenized classes
instead of gap-600 and max-h-[600px].
🪄 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
Run ID: db695a7c-f18a-44cb-a393-3aed0bd1349b
📒 Files selected for processing (14)
src/app/(private)/[clubId]/admin/layout.tsxsrc/app/(private)/[clubId]/error.tsxsrc/app/api/proxy/auth/refresh/route.tssrc/components/admin/member/MemberTable.tsxsrc/components/admin/member/MemberTopBar.tsxsrc/components/admin/member/modal/AddCardinalModal.tsxsrc/components/admin/member/modal/ChangeCardinalsModal.tsxsrc/components/attendance/AttendanceTodayCard.tsxsrc/components/home/HomeBoardContent.tsxsrc/components/layout/header/DefaultActions.tsxsrc/constants/home/tutorial.tsxsrc/constants/term.tssrc/utils/shared/getApiErrorCode.tssrc/utils/shared/runBulkMutation.ts
✅ Files skipped from review due to trivial changes (4)
- src/components/home/HomeBoardContent.tsx
- src/components/attendance/AttendanceTodayCard.tsx
- src/components/layout/header/DefaultActions.tsx
- src/constants/home/tutorial.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
- src/utils/shared/getApiErrorCode.ts
- src/constants/term.ts
- src/app/(private)/[clubId]/admin/layout.tsx
- src/utils/shared/runBulkMutation.ts
| const isAllSelected = members.length > 0 && selectedIds.size === members.length; | ||
| const hasAnySelected = selectedIds.size > 0; |
There was a problem hiding this comment.
현재 멤버 기준으로 선택 상태를 계산해 주세요.
selectedIds.size만 보면 필터/빈 목록에서 현재 보이지 않는 ID까지 체크 상태로 집계됩니다. 그 결과 헤더 아이콘과 aria-pressed가 서로 어긋날 수 있습니다. members와의 교집합 기준으로 hasAnySelected/isAllSelected를 계산하거나, 멤버 목록이 바뀔 때 선택 상태를 정리해 주세요.
🔧 제안 수정
- const isAllSelected = members.length > 0 && selectedIds.size === members.length;
- const hasAnySelected = selectedIds.size > 0;
+ const selectedMemberIds = members.filter((member) => selectedIds.has(member.id));
+ const isAllSelected = members.length > 0 && selectedMemberIds.length === members.length;
+ const hasAnySelected = selectedMemberIds.length > 0;Also applies to: 99-104
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/admin/member/MemberTable.tsx` around lines 53 - 54, Compute
selection state against the currently visible members instead of using
selectedIds.size directly: derive a visibleSelectedCount by counting members
whose id exists in selectedIds (e.g., members.filter(m =>
selectedIds.has(m.id)).length) and then set isAllSelected = members.length > 0
&& visibleSelectedCount === members.length and hasAnySelected =
visibleSelectedCount > 0; alternatively, when members changes clear or intersect
selectedIds to keep only visible IDs. Apply the same change to the other similar
selection calculations in this file (the block around the repeated logic).
| <TableRow | ||
| key={member.id} | ||
| className="hover:bg-container-neutral-interaction cursor-pointer border-0" | ||
| onClick={() => onMemberAction?.(member)} | ||
| > |
There was a problem hiding this comment.
행 클릭에 키보드 접근성을 추가해 주세요.
TableRow는 클릭으로 모달을 열지만, <tr>에 onClick만 붙어 있어 키보드 사용자는 같은 동작을 수행할 수 없습니다. 이 변경은 행 전체를 상호작용 요소로 만들었으니 tabIndex/Enter/Space 처리 같은 대체 수단이 필요합니다.
⌨️ 제안 수정
<TableRow
key={member.id}
className="hover:bg-container-neutral-interaction cursor-pointer border-0"
onClick={() => onMemberAction?.(member)}
+ tabIndex={0}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ onMemberAction?.(member);
+ }
+ }}
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <TableRow | |
| key={member.id} | |
| className="hover:bg-container-neutral-interaction cursor-pointer border-0" | |
| onClick={() => onMemberAction?.(member)} | |
| > | |
| <TableRow | |
| key={member.id} | |
| className="hover:bg-container-neutral-interaction cursor-pointer border-0" | |
| onClick={() => onMemberAction?.(member)} | |
| tabIndex={0} | |
| onKeyDown={(e) => { | |
| if (e.key === 'Enter' || e.key === ' ') { | |
| e.preventDefault(); | |
| onMemberAction?.(member); | |
| } | |
| }} | |
| > |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/admin/member/MemberTable.tsx` around lines 116 - 120, The
TableRow currently only handles mouse clicks via onClick and is not
keyboard-accessible; update the TableRow element (the one with key={member.id}
and onClick={() => onMemberAction?.(member)}) to be focusable and respond to
Enter/Space: add tabIndex={0}, role="button" (or appropriate ARIA role), and
implement an onKeyDown handler that calls onMemberAction?.(member) when the user
presses Enter or Space (for Space, preventDefault to avoid scrolling). Keep the
existing onClick behavior and visual cursor style.




✅ PR 유형
어떤 변경 사항이 있었나요?
📌 관련 이슈번호
✅ Key Changes
🆕 새 기능
🐛 버그 / QA 수정
UserHydrator추가)useUserStore.setRole('ADMIN')즉시 반영 + 서버 dashboard 태그 캐시updateTag로 무효화members.length > 0가드 추가)min-w-3xl→min-w-0로 변경하여 일정관리 페이지와 동일한 스크롤 패턴 적용role필드 제거 — 실제 역할 표시는position만 사용 (헤더 '역할' 중복 라벨 해결)LEAD→리더, description 일관성 유지♻️ 리팩토링
CardinalPillList로 추출getApiErrorCode(err)— axios 에러 코드 추출 패턴 통합parseCardinals(raw)— 활동기수 문자열 파싱 통합runBulkMutation에 에러별 메시지 오버라이드 콜백 추가 — 21114 같은 특정 에러 코드 감지 시 다른 토스트 노출 가능MemberTopBar/MemberDetailModal의 리더 변경 액션을 액션 배열로 통합 — 인라인 JSX 제거, 일관된 렌더링 패턴Table컴포넌트에wrapperClassNameprop 추가 — sticky thead 지원useUserStore에setRole액션 추가 — 직접setState호출 제거 (store 디자인 컨벤션 준수)MEMBER_ROLE_ERROR_CODE,CARDINAL_ERROR_CODEuseEffect로 변경 — 콜백 ref의 재호출 미보장 이슈 해결revalidateTag시그니처 변경에 맞춰updateTag로 교체📸 스크린샷 or 실행영상
🎸 기타 사항 or 추가 코멘트
member.cardinal이 다중 기수 문자열일 때 멤버 상세 모달에서 첫 숫자만 표시되는 부분에 TODO 주석 추가 (응답 정렬 확인 후 수정 예정)하 왜 기수 추가하고 추가한 기수로 유저 기수 변경하는 게 안 될까요,,?? 저만 이런 걸까요..?? ㅜㅜ
파일 체인지는 이제 네이밍 한 번에 바꾸기 하면서 다른 파일들도 건들게 돼 가지구,, 이름 때문에 체크된 부분 훅훅 넘기시면 생각보다 별 거 없을 겁니다,,, ㅎㅎ,,
Summary by CodeRabbit
새로운 기능
개선사항