[Feat] WTH-234: 마이페이지 API 연결#40
Conversation
…into feat/WTH-215-멤버-어드민-API-연결
There was a problem hiding this comment.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
.claude/settings.local.json (1)
6-23:⚠️ Potential issue | 🔴 Critical
.claude/settings.local.json에서 Git 머지 충돌 마커를 해소하세요.6번째, 20번째, 23번째 줄의 충돌 마커(
<<<<<<<,=======,>>>>>>>)로 인해 JSON이 유효하지 않습니다. 충돌을 해소하고 하나의 최종allow목록만 유지해야 합니다.해소 예시
-<<<<<<< HEAD - "mcp__figma__get_design_context", - "Bash(grep -E \"\\\\.tsx$\")", - "Bash(find \"D:\\\\project\\\\weeth-client\\\\src\\\\app\\\\\\(private\\)\" -name \"layout.tsx\" -type f)", - "Bash(find \"D:\\\\project\\\\weeth-client\\\\src\\\\app\\\\\\(private\\)\\\\\\(main\\)\" -name \"page.tsx\" -type f)", - "Bash(grep -r PageNavigation D:projectweeth-clientsrc --include=*.tsx --include=*.ts)", - "Bash(ls -la \"D:\\\\project\\\\weeth-client\\\\src\\\\app\\\\\\(private\\)\\\\\\(intro\\)\\\\home\")", - "Bash(find \"D:\\\\project\\\\weeth-client\\\\src\\\\app\\\\\\(private\\)\" -type f -name \"layout.tsx\")", - "mcp__figma__get_screenshot", - "Bash(find D:projectweeth-clientsrccomponentsmypage -type f -name *.tsx -o -name *.ts)", - "Bash(ls -la \"D:\\\\project\\\\weeth-client\\\\src\\\\app\\\\\\(private\\)\\\\\\(main\\)\\\\mypage\")", - "Bash(ls -la \"D:\\\\project\\\\weeth-client\\\\src\\\\app\\\\\\(private\\)\\\\\\(main\\)\\\\mypage\\\\edit\")", - "Bash(find D:projectweeth-clientsrccomponentsui -type f \\\\\\(-name *.tsx -o -name *.ts \\\\\\))", - "Bash(find D:/project/weeth-client/src/constants -name *.ts)" -======= - "Bash(ls \"D:/project/weeth-client/src/app/\\(public\\)/\\(landing\\)/\")", - "Bash(git fetch:*)" ->>>>>>> feat/WTH-215-멤버-어드민-API-연결 + "...최종 합의된 항목들..."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.claude/settings.local.json around lines 6 - 23, The JSON contains unresolved Git merge conflict markers (<<<<<<<, =======, >>>>>>>) around the "allow" entries (e.g., entries like "mcp__figma__get_design_context", "Bash(ls \"D:/project/weeth-client/src/app/\\(public\\)/\\(landing\\)/\")", "Bash(git fetch:*)"); remove the conflict markers, pick and retain the intended final set of allow entries (merging or deduplicating the two sides as appropriate), ensure the result is a single valid JSON array/object (no leftover markers, proper commas/quotes), and save so the file parses as valid JSON.src/components/admin/member/MemberPageContent.tsx (1)
54-76:⚠️ Potential issue | 🟠 Major혼합 상태 선택에서 잘못된 일괄 액션이 실행됩니다.
지금 분기는
allBanned만 보고 결정해서, 활성 멤버와BANNED멤버를 함께 선택해도 “유저 추방”이 활성화되고 이미 정지된 멤버에게banMember를 다시 호출합니다. 상태가 섞인 경우에는 액션을 비활성화하거나, ban/restore 대상을 상태별로 분리해서 호출해야 합니다.🔧 제안 수정안
+ const bannableMembers = selectedMembers.filter((m) => m.status !== 'BANNED'); + const restorableMembers = selectedMembers.filter((m) => m.status === 'BANNED'); const selectedCount = selectedMembers.length; @@ - const allBanned = selectedCount > 0 && selectedMembers.every((m) => m.status === 'BANNED'); + const allBanned = selectedCount > 0 && restorableMembers.length === selectedCount; @@ - onBan={() => selectedMembers.forEach((m) => banMember(m.clubMemberId))} - onRestore={() => selectedMembers.forEach((m) => restoreMember(m.clubMemberId))} + onBan={ + bannableMembers.length === selectedCount + ? () => bannableMembers.forEach((m) => banMember(m.clubMemberId)) + : undefined + } + onRestore={ + restorableMembers.length === selectedCount + ? () => restorableMembers.forEach((m) => restoreMember(m.clubMemberId)) + : undefined + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/admin/member/MemberPageContent.tsx` around lines 54 - 76, The current logic uses allBanned to decide actions and blindly calls banMember/restoreMember for every selectedMembers, causing redundant calls when selection has mixed statuses; update the MemberTopBar handlers so they either disable the action when selection is mixed (selectedCount>0 && !allBanned && !allActive) or, if keeping action enabled, split selectedMembers by m.status and call banMember only for members with status !== 'BANNED' and restoreMember only for members with status === 'BANNED'; likewise guard changeMemberRole to only call changeMemberRole for members whose role actually differs from targetRole; use the existing symbols selectedMembers, allBanned, selectedCount, targetRole, banMember, restoreMember, and changeMemberRole to implement the filtering or disabling behavior.
🧹 Nitpick comments (13)
src/components/mypage/MyPageDropdownMenu.tsx (2)
40-57: 주석 처리된 코드 정리 권장서비스 탈퇴 기능 관련 코드가 주석 처리되어 있습니다. 나중에 구현할 계획이라면 이 코드를 삭제하고 별도 이슈로 추적하는 것이 코드 가독성과 유지보수에 좋습니다. 주석 처리된 코드는 버전 관리 시스템에서 이력으로 남기 때문에 제거해도 무방합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/mypage/MyPageDropdownMenu.tsx` around lines 40 - 57, Remove the commented-out "서비스 탈퇴" block and the disabled AlertDialog modal code to improve readability: delete the commented DropdownMenuItem that references setWithdrawOpen / 서비스 탈퇴 and the AlertDialog block (props open/onOpenChange, status/title/description, AlertDialogAction/AlertDialogCancel) and any unused state or imports (e.g., withdrawOpen, setWithdrawOpen) from MyPageDropdownMenu.tsx; if the feature will be implemented later, create a tracking issue instead of leaving commented code.
19-19: 사용되지 않는withdrawOpen상태 제거 필요서비스 탈퇴 기능이 주석 처리되면서
withdrawOpen상태가 선언만 되고 사용되지 않습니다. 불필요한 코드를 제거하세요.♻️ 제안된 수정
- const [withdrawOpen, setWithdrawOpen] = useState(false); const [logoutOpen, setLogoutOpen] = useState(false);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/mypage/MyPageDropdownMenu.tsx` at line 19, The declared state withdrawOpen (and its setter setWithdrawOpen) in MyPageDropdownMenu is unused because the withdraw feature was commented out; remove the unused useState declaration (the const [withdrawOpen, setWithdrawOpen] = useState(false) line) and any related unused imports if they become orphaned (e.g., useState) to clean up the component and avoid linter warnings.src/components/mypage/edit/PersonalInfoFields.tsx (1)
1-1: 사용되지 않는Control타입 import 제거
Control타입이 import되었지만 컴포넌트에서 사용되지 않습니다.♻️ 제안된 수정
-import type { Control, FieldErrors, UseFormRegister, UseFormSetValue } from 'react-hook-form'; +import type { FieldErrors, UseFormRegister, UseFormSetValue } from 'react-hook-form';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/mypage/edit/PersonalInfoFields.tsx` at line 1, The import of the Control type from 'react-hook-form' is unused in PersonalInfoFields.tsx; remove Control from the import list so the line imports only FieldErrors, UseFormRegister, and UseFormSetValue, and then run your linter/TS check to ensure no other references to Control remain (update any code if Control was intended to be used).src/lib/schemas/editProfile.ts (1)
10-13: 이메일 검증에.email()메서드 사용 권장
refine()과 커스텀 정규식 대신 Zod의 내장.email()메서드를 사용하면 더 간결하고 표준적인 검증이 가능합니다.♻️ 제안된 수정
email: z .string() .min(1, '이메일을 입력해주세요') - .refine((v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), '올바른 이메일 형식이 아닙니다'), + .email('올바른 이메일 형식이 아닙니다'),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/schemas/editProfile.ts` around lines 10 - 13, The email field in the editProfile Zod schema uses a custom .refine with a regex; replace that with Zod's built-in .email() to use standard validation: update the email schema (the email property inside the editProfile schema) to call .email('올바른 이메일 형식이 아닙니다') and use .nonempty('이메일을 입력해주세요') (or keep the existing .min(1, ...)) instead of the regex-based .refine to simplify and standardize validation.src/lib/apis/server.ts (1)
75-77: 개발용 토큰 fallback 조건을 조금 더 제한하는 것이 안전합니다.Line 75-77 로직은
ACCESS_TOKEN_KEY가 없을 때 항상DEV_ACCESS_TOKEN을 사용합니다. 이때REFRESH_TOKEN_KEY가 남아있는 세션에서도 다른 계정 토큰으로 요청이 성공해, 원래 refresh 경로를 타지 못할 수 있습니다. 개발 환경에서도 세션 일관성을 위해 fallback을 더 좁히는 편이 좋습니다.제안 diff
- const accessToken = - cookieStore.get(ACCESS_TOKEN_KEY)?.value ?? - (process.env.NODE_ENV === 'development' ? process.env.DEV_ACCESS_TOKEN : undefined); + const accessTokenFromCookie = cookieStore.get(ACCESS_TOKEN_KEY)?.value; + const refreshTokenFromCookie = cookieStore.get(REFRESH_TOKEN_KEY)?.value; + const devAccessToken = + process.env.NODE_ENV === 'development' && !refreshTokenFromCookie + ? process.env.DEV_ACCESS_TOKEN + : undefined; + const accessToken = accessTokenFromCookie ?? devAccessToken;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/apis/server.ts` around lines 75 - 77, The current accessToken fallback uses DEV_ACCESS_TOKEN whenever ACCESS_TOKEN_KEY is missing, which can hide an existing REFRESH_TOKEN and cause mismatched session behavior; update the logic around accessToken (the cookieStore.get(ACCESS_TOKEN_KEY) resolution) to only use process.env.DEV_ACCESS_TOKEN in development when there is no refresh token present (check cookieStore.get(REFRESH_TOKEN_KEY)?.value) and/or when no session cookies exist, so the DEV_ACCESS_TOKEN is applied only for truly session-less dev requests and not when a refresh token is available.src/hooks/mutations/mypage/useInitCardinalsMutation.ts (1)
14-18:onSuccess내부의clubId체크는 불필요합니다.
mutationFn에서 이미clubId가 없으면 에러를 throw하므로,onSuccess에 도달했다면clubId는 항상 truthy입니다. 다만 defensive coding으로 남겨두어도 큰 문제는 없습니다.♻️ 선택적 개선안
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['mypage', 'clubs'] }); - if (clubId) { - queryClient.invalidateQueries({ queryKey: ['mypage', 'me', clubId] }); - } + queryClient.invalidateQueries({ queryKey: ['mypage', 'me', clubId] }); },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/hooks/mutations/mypage/useInitCardinalsMutation.ts` around lines 14 - 18, In useInitCardinalsMutation's onSuccess handler remove the unnecessary clubId guard (the mutationFn already throws when clubId is missing) and always call queryClient.invalidateQueries({ queryKey: ['mypage', 'me', clubId] }) alongside the existing ['mypage','clubs'] invalidation; update the onSuccess block to directly invalidate both queries without the if (clubId) check so the code is simpler and consistent with mutationFn's preconditions.src/components/mypage/MyPageContent.tsx (1)
94-96: 가로 스크롤 또는 빈 상태 UI를 고려하세요.
clubs가 빈 배열일 때 "활동정보" 섹션이 비어 보일 수 있습니다. 클럽이 없는 경우에 대한 안내 메시지 추가를 고려해볼 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/mypage/MyPageContent.tsx` around lines 94 - 96, MyPageContent renders clubs.map(...) with ClubInfoCard and doesn't handle the empty or overflow case; update the JSX in the component that uses the clubs array (the clubs variable and ClubInfoCard list) to conditionally render a clear empty-state UI when clubs.length === 0 (e.g., a centered message/icon like "가입한 클럽이 없습니다" or a dedicated EmptyState component) and ensure the container styling prevents unwanted horizontal scrolling (add a wrapping element with overflow-x-hidden or appropriate flex/wrap behavior around the ClubInfoCard list).src/utils/admin/memberMapper.ts (1)
3-7:ROLE_MAP상수 중복 제거 고려
ROLE_MAP이useAdminMemberMutations.ts의ROLE_LABEL과 동일한 매핑을 정의하고 있습니다. 공통 상수 파일로 추출하여 중복을 제거하는 것을 권장합니다.♻️ 제안: 공통 상수로 추출
// src/constants/admin/memberRole.constants.ts import type { ClubMemberRole } from '@/types/admin/member'; export const ROLE_LABEL: Record<ClubMemberRole, string> = { USER: '사용자', ADMIN: '관리자', LEAD: '리더', };그 후
memberMapper.ts와useAdminMemberMutations.ts에서 import하여 사용할 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/utils/admin/memberMapper.ts` around lines 3 - 7, ROLE_MAP duplicates ROLE_LABEL; extract a single shared constant (e.g. export const ROLE_LABEL: Record<ClubMemberRole,string>) into a common constants module and replace both ROLE_MAP (in memberMapper.ts) and ROLE_LABEL (in useAdminMemberMutations.ts) with imports from that module, keeping the ClubMemberRole type and the same mapping values to avoid behavioral changes.src/components/mypage/SetCardinalModal/index.tsx (1)
70-70:px-2.5대신 디자인 토큰 사용 고려
px-2.5는 하드코딩된 값입니다. 프로젝트의 디자인 토큰 패딩 클래스(예:px-200,px-300)로 대체하는 것을 고려해 주세요. As per coding guidelines, "Always use design token classes first — no hardcoded values".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/mypage/SetCardinalModal/index.tsx` at line 70, The div in the SetCardinalModal component uses a hardcoded padding class "px-2.5" which violates the design-token rule; replace that hardcoded class with the appropriate design token padding class (e.g., "px-200" or "px-300") in the className on the element inside SetCardinalModal (look for the div with className="flex items-center gap-400 px-2.5 pt-200") so the component uses the project's design token utility classes consistently.src/hooks/mutations/admin/useAdminMemberMutations.ts (1)
63-63: 상태 값을 타입 상수로 관리 고려
'BANNED'와'ACTIVE'문자열이 하드코딩되어 있습니다.MemberStatus타입과 연계된 상수 객체를 사용하면 타입 안전성과 유지보수성이 향상됩니다.♻️ 제안
+import { MemberStatus } from '@/types/admin/member'; + +const STATUS = { + ACTIVE: 'ACTIVE', + BANNED: 'BANNED', +} as const satisfies Partial<Record<MemberStatus, MemberStatus>>; // useBanMember 내부 - old.map((m) => (m.clubMemberId === clubMemberId ? { ...m, status: 'BANNED' } : m)), + old.map((m) => (m.clubMemberId === clubMemberId ? { ...m, status: STATUS.BANNED } : m)),Also applies to: 91-91
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/hooks/mutations/admin/useAdminMemberMutations.ts` at line 63, Replace hardcoded status strings in the update logic with the project-wide MemberStatus constant/enum: where old.map((m) => (m.clubMemberId === clubMemberId ? { ...m, status: 'BANNED' } : m)) and the similar occurrence at line 91 currently use string literals ('BANNED'/'ACTIVE'); import or reference MemberStatus (or STATUS constant object) and use MemberStatus.BANNED and MemberStatus.ACTIVE instead to ensure type safety and consistency across useAdminMemberMutations.ts.src/components/mypage/edit/EditProfileContent.tsx (1)
116-118: 임의 px 값이 토큰 규칙을 우회합니다.
max-w-[1088px],gap-[35px],pb-[80px],max-w-[640px]는 현재 디자인 토큰 체계를 벗어납니다. 기존 size/spacing 토큰으로 치환하거나 필요한 토큰을 먼저 추가하는 편이 좋겠습니다.As per coding guidelines "Always use design token classes first (text-, bg-, typo-, p-, gap-*) — no hardcoded values; ask before adding new tokens".
Also applies to: 149-149
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/mypage/edit/EditProfileContent.tsx` around lines 116 - 118, The container in EditProfileContent (the className passed into the cn call inside the EditProfileContent component) uses hardcoded bracket utilities like max-w-[1088px], gap-[35px], pb-[80px], px-450 and pt-450 which bypass the design tokens; replace those with the corresponding existing design token classes (e.g., max-w-*, gap-*, p-*/px-*/pt-*/pb-* tokens) or, if no appropriate token exists, request/add the new spacing/size token and then use it, and ensure the same change is applied to the other occurrence flagged (line ~149) so all hardcoded values are removed.src/constants/admin/memberDetailModal.constants.ts (2)
3-8: STATUS_LABEL 값이 한국어로 필요한지 확인이 필요합니다.현재 라벨이 영문 상태값과 동일합니다 (
ACTIVE,WAITING등). UI에서 한국어 표시가 필요하다면 라벨을 한국어로 변경하는 것을 고려해 주세요.💡 한국어 라벨 예시
export const STATUS_LABEL: Record<MemberStatus, string> = { - ACTIVE: 'ACTIVE', - WAITING: 'WAITING', - BANNED: 'BANNED', - LEFT: 'LEFT', + ACTIVE: '활동중', + WAITING: '대기중', + BANNED: '추방됨', + LEFT: '탈퇴', };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/constants/admin/memberDetailModal.constants.ts` around lines 3 - 8, STATUS_LABEL currently maps MemberStatus keys to identical English strings; if the UI requires Korean labels, update the STATUS_LABEL constant to map each MemberStatus (e.g., ACTIVE, WAITING, BANNED, LEFT) to the appropriate Korean string equivalents, keeping the keys as the enum names but replacing the values with Korean labels and ensure any code that consumes STATUS_LABEL (e.g., UI components rendering member status) continues to use these keys unchanged.
46-54: 인터페이스와 실제 사용이 일치하지 않습니다.
onApprove(line 49)와onResetPassword(line 51)가 인터페이스에 정의되어 있지만,getFooterActions함수에서는 주석 처리되어 사용되지 않습니다. 이로 인해 호출자가 불필요한 핸들러를 전달해야 할 수 있습니다.TODO 항목이므로 당장 삭제하기 어렵다면, 주석으로 명시하거나 인터페이스에서 제거하는 것을 권장합니다.
♻️ 수정 제안
interface FooterActionHandlers { memberRole: ClubMemberRole; status: MemberStatus; - onApprove?: () => void; + // TODO: 가입 승인 API 열리면 추가 + // onApprove?: () => void; onChangeRole?: () => void; - onResetPassword?: () => void; + // TODO: 비번 변경 API 열리면 추가 + // onResetPassword?: () => void; onBan?: () => void; onRestore?: () => void; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/constants/admin/memberDetailModal.constants.ts` around lines 46 - 54, The FooterActionHandlers interface declares onApprove and onResetPassword while getFooterActions currently comments them out; synchronize them by either removing onApprove and onResetPassword from FooterActionHandlers or reintroducing/using these handlers in getFooterActions so the interface matches usage—update the FooterActionHandlers declaration (or uncomment and wire the handlers in getFooterActions) and add a brief comment explaining why the handlers are present/unused to avoid confusion.
🤖 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/MemberPageContent.tsx`:
- Around line 144-155: The onChangeRole handler currently toggles roles by
treating anything not 'ADMIN' as 'USER', causing 'LEAD' to be converted to
'ADMIN'; replace the boolean toggle with an explicit mapping for nextRole (e.g.,
NEXT_ROLE = { ADMIN: 'USER', USER: 'ADMIN', LEAD: 'LEAD' } or block changes for
'LEAD') and use that mapping when computing nextRole, then update
setDetailMember (preserving memberRole and position via ROLE_LABEL[nextRole])
and call changeMemberRole({ clubMemberId: detailMember.clubMemberId, memberRole:
nextRole }) only for allowed transitions; update references to onChangeRole,
detailMember, nextRole, ROLE_LABEL, setDetailMember, and changeMemberRole
accordingly.
In `@src/components/admin/member/MemberTable.tsx`:
- Line 49: The isAllSelected computation incorrectly returns true when members
is an empty array; update the logic in MemberTable so isAllSelected only becomes
true when there are members and all are selected (i.e., require members.length >
0 in the equality check). Locate the isAllSelected declaration and change the
condition that compares selectedIds.size and members.length to also verify
members.length > 0 so the "select all" checkbox is not checked when there are no
members.
In `@src/components/admin/member/modal/MemberDetailModal.tsx`:
- Around line 145-158: The footerActions array can contain items with undefined
handlers causing a runtime error when AlertDialogAction calls onClick={handler};
update the rendering to only render actions with a valid handler or ensure
handlers are always defined: either filter footerActions before mapping (e.g.,
footerActions.filter(a => a.handler)) or change getFooterActions to assign a
no-op function as default for any missing onChangeRole/onBan/onRestore; target
symbols: footerActions, getFooterActions, AlertDialogAction, and the handler
prop used in the map.
In `@src/components/mypage/edit/EditProfileContent.tsx`:
- Around line 34-35: The component currently renders a perpetual "로딩 중..."
whenever me is falsy; adjust EditProfileContent to distinguish loading, success,
and error states by using the query status (from useMyMemberQuery) instead of
only checking me, and by separating update mutation state
(useUpdateProfileMutation's isPending) from query errors; specifically, check
the query's isLoading/isError (or error) and render an error or "no clubId" UI
when appropriate, render the loading indicator only when the query is truly
loading, and keep isPending for showing mutation-in-progress UI while allowing
API error UI to surface when the query fails. Ensure you update any other
similar logic referenced around lines 106-112 that also rely solely on me being
truthy.
In `@src/components/mypage/edit/PersonalInfoFields.tsx`:
- Around line 17-20: handlePhoneChange currently calls telOnChange(e) after
setValue('tel', formatPhone(...), { shouldValidate: true }), which overwrites
the formatted value with the raw event value and breaks the schema regex; remove
the telOnChange(e) invocation from handlePhoneChange so only setValue('tel',
formatPhone(e.target.value), { shouldValidate: true }) runs (leave formatPhone
and setValue usage intact to perform formatting and validation).
In `@src/components/mypage/edit/ProfileImageEditor.tsx`:
- Around line 18-30: The current preview URL cleanup can leak the previous
object URL because useEffect's cleanup runs with the latest previewUrl; modify
handleChange in ProfileImageEditor so it revokes any existing previewUrl before
creating a new one: when a new file is selected in handleChange, if previewUrl
exists call URL.revokeObjectURL(previewUrl) then
setPreviewUrl(URL.createObjectURL(file)) and call onFileChange(file);
alternatively track the previous URL in a ref (e.g., prevPreviewRef) and revoke
the old value there when updating to the new URL.
In `@src/constants/admin/memberDetailModal.constants.ts`:
- Around line 44-45: The import of the type ClubMemberRole is located mid-file;
move the "import type { ClubMemberRole } from '@/types/admin/member';" statement
up into the top import block with the other imports (as a type-only import) and
remove the duplicate/mid-file import occurrence so all imports reside at the top
and the rest of the file references ClubMemberRole normally.
In `@src/constants/admin/memberTable.constants.ts`:
- Line 38: The comparator currently uses parseInt(b.generation, 10) -
parseInt(a.generation, 10) which only considers the first cardinal; update the
comparator in memberTable.constants.ts to split each generation string (e.g.,
"1, 2, 3") into an array of numbers (trim and parse each element) and then
compare those arrays lexicographically (compare element 0, then 1, etc., falling
back to length if all equal). Alternatively, if the intended behavior is to sort
only by the first cardinal, explicitly parse and use only the first element
(e.g., split and parse index 0) and add a comment clarifying that choice;
reference the existing comparator expression to locate where to change.
In `@src/hooks/mutations/admin/useAdminCardinalMutations.ts`:
- Around line 3-4: 현재 파일이 존재하지 않는 CLUB_ID를 import하고 있어 빌드 오류가 발생합니다; 대신
useClubId 훅을 import해 동적으로 clubId를 사용하고 useMutation/useQueryClient 패턴(예:
useInitCardinalsMutation)을 따르세요. 구체적으로는 CLUB_ID import를 제거하고 useClubId를 불러와
clubId를 얻은 뒤 useMutation의 mutationFn에서 cardinalApi.createCardinal(clubId, body)를
호출하고 onSuccess에서 queryClient.invalidateQueries({ queryKey: ['cardinals', clubId]
})를 호출하도록 useCreateCardinal(또는 해당 훅 이름) 구현을 수정하세요.
In `@src/hooks/mutations/mypage/useUpdateProfileMutation.ts`:
- Around line 14-21: The upload flow in useUpdateProfileMutation doesn't guard
against missing presigned data or failed HTTP PUTs: check that
fileApi.getPresignedUrls returned a valid presigned entry (res.data.data[0] /
presigned) and if missing, abort and surface an error; after calling
fetch(presigned.putUrl, ...) verify the response.ok (and response.status) and
throw or return an error when not OK so updateClubProfile is not called with a
broken storageKey; make these checks around the existing
fileApi.getPresignedUrls call and the fetch call to reliably detect and handle
upload failures.
In `@src/types/admin/cardinal.ts`:
- Around line 9-12: The CreateCardinalBody interface is missing fields used by
the mutation; update the CreateCardinalBody type to include year and semester so
it matches the payload used in useAdminCardinalMutations.ts (i.e., define
CreateCardinalBody with cardinalNumber, year, semester, and inProgress), or
alternatively change useAdminCardinalMutations.ts to only send the fields
currently defined on CreateCardinalBody—ensure the symbol CreateCardinalBody and
the mutation functions in useAdminCardinalMutations.ts agree on the same shape.
In `@src/types/mypage.ts`:
- Around line 4-18: Update the MyMember interface so fields that can be absent
or null are typed accordingly: change tel, bio, and profileImageUrl on MyMember
to accept null/undefined (e.g. tel?: string | null, bio?: string | null,
profileImageUrl?: string | null) so usage sites like me.tel?.replace(...),
me.bio ?? '' and conditional checks on profileImageUrl align with the type.
Adjust only the MyMember declaration to reflect these nullable/optional types.
In `@src/utils/admin/memberMapper.ts`:
- Line 15: memberMapper.ts currently sets the mapped object's role field to an
empty string causing blank UI; update the mapper (the function that builds the
mapped member object in memberMapper.ts) to assign role from the appropriate
source (e.g., ROLE_MAP[cm.memberRole] or the same value used for position) or
remove/stop exposing role if unused by getPersonalInfo in
memberDetailModal.constants.ts — locate the mapping that sets "role: ''" and
either replace the empty string with ROLE_MAP[cm.memberRole] (or the correct
member property) or remove the role property and adjust consumer code in
getPersonalInfo accordingly.
---
Outside diff comments:
In @.claude/settings.local.json:
- Around line 6-23: The JSON contains unresolved Git merge conflict markers
(<<<<<<<, =======, >>>>>>>) around the "allow" entries (e.g., entries like
"mcp__figma__get_design_context", "Bash(ls
\"D:/project/weeth-client/src/app/\\(public\\)/\\(landing\\)/\")", "Bash(git
fetch:*)"); remove the conflict markers, pick and retain the intended final set
of allow entries (merging or deduplicating the two sides as appropriate), ensure
the result is a single valid JSON array/object (no leftover markers, proper
commas/quotes), and save so the file parses as valid JSON.
In `@src/components/admin/member/MemberPageContent.tsx`:
- Around line 54-76: The current logic uses allBanned to decide actions and
blindly calls banMember/restoreMember for every selectedMembers, causing
redundant calls when selection has mixed statuses; update the MemberTopBar
handlers so they either disable the action when selection is mixed
(selectedCount>0 && !allBanned && !allActive) or, if keeping action enabled,
split selectedMembers by m.status and call banMember only for members with
status !== 'BANNED' and restoreMember only for members with status === 'BANNED';
likewise guard changeMemberRole to only call changeMemberRole for members whose
role actually differs from targetRole; use the existing symbols selectedMembers,
allBanned, selectedCount, targetRole, banMember, restoreMember, and
changeMemberRole to implement the filtering or disabling behavior.
---
Nitpick comments:
In `@src/components/mypage/edit/EditProfileContent.tsx`:
- Around line 116-118: The container in EditProfileContent (the className passed
into the cn call inside the EditProfileContent component) uses hardcoded bracket
utilities like max-w-[1088px], gap-[35px], pb-[80px], px-450 and pt-450 which
bypass the design tokens; replace those with the corresponding existing design
token classes (e.g., max-w-*, gap-*, p-*/px-*/pt-*/pb-* tokens) or, if no
appropriate token exists, request/add the new spacing/size token and then use
it, and ensure the same change is applied to the other occurrence flagged (line
~149) so all hardcoded values are removed.
In `@src/components/mypage/edit/PersonalInfoFields.tsx`:
- Line 1: The import of the Control type from 'react-hook-form' is unused in
PersonalInfoFields.tsx; remove Control from the import list so the line imports
only FieldErrors, UseFormRegister, and UseFormSetValue, and then run your
linter/TS check to ensure no other references to Control remain (update any code
if Control was intended to be used).
In `@src/components/mypage/MyPageContent.tsx`:
- Around line 94-96: MyPageContent renders clubs.map(...) with ClubInfoCard and
doesn't handle the empty or overflow case; update the JSX in the component that
uses the clubs array (the clubs variable and ClubInfoCard list) to conditionally
render a clear empty-state UI when clubs.length === 0 (e.g., a centered
message/icon like "가입한 클럽이 없습니다" or a dedicated EmptyState component) and ensure
the container styling prevents unwanted horizontal scrolling (add a wrapping
element with overflow-x-hidden or appropriate flex/wrap behavior around the
ClubInfoCard list).
In `@src/components/mypage/MyPageDropdownMenu.tsx`:
- Around line 40-57: Remove the commented-out "서비스 탈퇴" block and the disabled
AlertDialog modal code to improve readability: delete the commented
DropdownMenuItem that references setWithdrawOpen / 서비스 탈퇴 and the AlertDialog
block (props open/onOpenChange, status/title/description,
AlertDialogAction/AlertDialogCancel) and any unused state or imports (e.g.,
withdrawOpen, setWithdrawOpen) from MyPageDropdownMenu.tsx; if the feature will
be implemented later, create a tracking issue instead of leaving commented code.
- Line 19: The declared state withdrawOpen (and its setter setWithdrawOpen) in
MyPageDropdownMenu is unused because the withdraw feature was commented out;
remove the unused useState declaration (the const [withdrawOpen,
setWithdrawOpen] = useState(false) line) and any related unused imports if they
become orphaned (e.g., useState) to clean up the component and avoid linter
warnings.
In `@src/components/mypage/SetCardinalModal/index.tsx`:
- Line 70: The div in the SetCardinalModal component uses a hardcoded padding
class "px-2.5" which violates the design-token rule; replace that hardcoded
class with the appropriate design token padding class (e.g., "px-200" or
"px-300") in the className on the element inside SetCardinalModal (look for the
div with className="flex items-center gap-400 px-2.5 pt-200") so the component
uses the project's design token utility classes consistently.
In `@src/constants/admin/memberDetailModal.constants.ts`:
- Around line 3-8: STATUS_LABEL currently maps MemberStatus keys to identical
English strings; if the UI requires Korean labels, update the STATUS_LABEL
constant to map each MemberStatus (e.g., ACTIVE, WAITING, BANNED, LEFT) to the
appropriate Korean string equivalents, keeping the keys as the enum names but
replacing the values with Korean labels and ensure any code that consumes
STATUS_LABEL (e.g., UI components rendering member status) continues to use
these keys unchanged.
- Around line 46-54: The FooterActionHandlers interface declares onApprove and
onResetPassword while getFooterActions currently comments them out; synchronize
them by either removing onApprove and onResetPassword from FooterActionHandlers
or reintroducing/using these handlers in getFooterActions so the interface
matches usage—update the FooterActionHandlers declaration (or uncomment and wire
the handlers in getFooterActions) and add a brief comment explaining why the
handlers are present/unused to avoid confusion.
In `@src/hooks/mutations/admin/useAdminMemberMutations.ts`:
- Line 63: Replace hardcoded status strings in the update logic with the
project-wide MemberStatus constant/enum: where old.map((m) => (m.clubMemberId
=== clubMemberId ? { ...m, status: 'BANNED' } : m)) and the similar occurrence
at line 91 currently use string literals ('BANNED'/'ACTIVE'); import or
reference MemberStatus (or STATUS constant object) and use MemberStatus.BANNED
and MemberStatus.ACTIVE instead to ensure type safety and consistency across
useAdminMemberMutations.ts.
In `@src/hooks/mutations/mypage/useInitCardinalsMutation.ts`:
- Around line 14-18: In useInitCardinalsMutation's onSuccess handler remove the
unnecessary clubId guard (the mutationFn already throws when clubId is missing)
and always call queryClient.invalidateQueries({ queryKey: ['mypage', 'me',
clubId] }) alongside the existing ['mypage','clubs'] invalidation; update the
onSuccess block to directly invalidate both queries without the if (clubId)
check so the code is simpler and consistent with mutationFn's preconditions.
In `@src/lib/apis/server.ts`:
- Around line 75-77: The current accessToken fallback uses DEV_ACCESS_TOKEN
whenever ACCESS_TOKEN_KEY is missing, which can hide an existing REFRESH_TOKEN
and cause mismatched session behavior; update the logic around accessToken (the
cookieStore.get(ACCESS_TOKEN_KEY) resolution) to only use
process.env.DEV_ACCESS_TOKEN in development when there is no refresh token
present (check cookieStore.get(REFRESH_TOKEN_KEY)?.value) and/or when no session
cookies exist, so the DEV_ACCESS_TOKEN is applied only for truly session-less
dev requests and not when a refresh token is available.
In `@src/lib/schemas/editProfile.ts`:
- Around line 10-13: The email field in the editProfile Zod schema uses a custom
.refine with a regex; replace that with Zod's built-in .email() to use standard
validation: update the email schema (the email property inside the editProfile
schema) to call .email('올바른 이메일 형식이 아닙니다') and use .nonempty('이메일을 입력해주세요') (or
keep the existing .min(1, ...)) instead of the regex-based .refine to simplify
and standardize validation.
In `@src/utils/admin/memberMapper.ts`:
- Around line 3-7: ROLE_MAP duplicates ROLE_LABEL; extract a single shared
constant (e.g. export const ROLE_LABEL: Record<ClubMemberRole,string>) into a
common constants module and replace both ROLE_MAP (in memberMapper.ts) and
ROLE_LABEL (in useAdminMemberMutations.ts) with imports from that module,
keeping the ClubMemberRole type and the same mapping values to avoid behavioral
changes.
🪄 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: 279476c4-e019-4102-af7a-abe36128e8ed
📒 Files selected for processing (46)
.claude/settings.local.jsonsrc/app/api/proxy/[...path]/route.tssrc/assets/icons/admin/index.tssrc/components/admin/member/MemberPageContent.tsxsrc/components/admin/member/MemberSearchBar.tsxsrc/components/admin/member/MemberTable.tsxsrc/components/admin/member/MemberTopBar.tsxsrc/components/admin/member/modal/MemberDetailModal.tsxsrc/components/mypage/ClubInfoCard.tsxsrc/components/mypage/EditProfileContent.tsxsrc/components/mypage/FormField.tsxsrc/components/mypage/MyPageContent.tsxsrc/components/mypage/MyPageDropdownMenu.tsxsrc/components/mypage/SetCardinalModal/index.tsxsrc/components/mypage/SetCardinalModal/useCardinalModal.tssrc/components/mypage/edit/EditProfileContent.tsxsrc/components/mypage/edit/PersonalInfoFields.tsxsrc/components/mypage/edit/ProfileImageEditor.tsxsrc/components/mypage/edit/SchoolInfoFields.tsxsrc/components/mypage/edit/index.tssrc/components/mypage/index.tssrc/constants/admin/memberDetailModal.constants.tssrc/constants/admin/memberTable.constants.tssrc/constants/admin/memberTopBar.constants.tssrc/hooks/mutations/admin/index.tssrc/hooks/mutations/admin/useAdminCardinalMutations.tssrc/hooks/mutations/admin/useAdminMemberMutations.tssrc/hooks/mutations/mypage/useInitCardinalsMutation.tssrc/hooks/mutations/mypage/useUpdateProfileMutation.tssrc/hooks/queries/admin/index.tssrc/hooks/queries/admin/useAdminMemberQueries.tssrc/hooks/queries/index.tssrc/hooks/queries/mypage/useMyClubsQuery.tssrc/hooks/queries/mypage/useMyMemberQuery.tssrc/hooks/queries/useCardinals.tssrc/lib/apis/adminMember.tssrc/lib/apis/cardinal.tssrc/lib/apis/index.tssrc/lib/apis/mypage.tssrc/lib/apis/server.tssrc/lib/schemas/editProfile.tssrc/stores/useClubStore.tssrc/types/admin/cardinal.tssrc/types/admin/member.d.tssrc/types/mypage.tssrc/utils/admin/memberMapper.ts
💤 Files with no reviewable changes (1)
- src/components/mypage/EditProfileContent.tsx
🤖 Claude 테스트 제안
변경된 컴포넌트에 대해 Claude가 생성한 테스트 코드입니다. 검토 후 적합한 부분만 사용하세요.
|
PR 테스트 결과✅ Jest: 통과 🎉 모든 테스트를 통과했습니다! |
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)
.claude/settings.local.json (1)
6-28:⚠️ Potential issue | 🔴 Critical해결되지 않은 Git 병합 충돌이 있습니다.
이 파일에 Git 병합 충돌 마커(
<<<<<<< HEAD,=======,>>>>>>>)가 그대로 남아있어 JSON 파싱이 불가능합니다. 이 상태로는 설정 파일을 읽을 수 없습니다.충돌을 해결하고 유효한 JSON 형식으로 수정해 주세요. 중첩된 충돌이 있는 것으로 보이므로 주의가 필요합니다:
- 첫 번째 충돌: Line 6-25 (
HEADvsfeat/WTH-215-멤버-어드민-API-연결)- 두 번째 충돌: Line 19-28 (중첩된 충돌, commit
70dadf52f3d996e43f43531b2e889a992079f87c)🐛 충돌 해결 후 예상되는 구조 (필요한 항목 선택 필요)
-<<<<<<< HEAD "mcp__figma__get_design_context", "Bash(grep -E \"\\\\.tsx$\")", ... -<<<<<<< HEAD - "Bash(find D:/project/weeth-client/src/constants -name *.ts)" -======= - "Bash(ls \"D:/project/weeth-client/src/app/\\(public\\)/\\(landing\\)/\")", - "Bash(git fetch:*)" ->>>>>>> feat/WTH-215-멤버-어드민-API-연결 -======= "Bash(find D:/project/weeth-client/src/constants -name *.ts)", "Bash(grep -r \"POSTHOG\\\\|PostHog\" D:projectweeth-client/.env*)" ->>>>>>> 70dadf52f3d996e43f43531b2e889a992079f87c🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.claude/settings.local.json around lines 6 - 28, This file contains unresolved Git merge conflict markers (<<<<<<< HEAD, =======, >>>>>>>) that break JSON; remove the conflict markers, pick/merge the desired entries from the conflicting blocks (e.g., the different "Bash(...)" lines and "mcp__figma__get_design_context"/"mcp__figma__get_screenshot" entries), ensure there are no duplicate or malformed entries, and produce a single valid JSON array/object with proper commas and quoting so the file parses; pay special attention to the nested conflict between the HEAD block and commits shown (resolve which of the "Bash(find ...)" and "Bash(ls ...)" / "Bash(git fetch:*)" / "Bash(grep -r ...)" variants to keep) and remove any leftover whitespace or stray characters from the merge markers.
🤖 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/mypage/edit/EditProfileContent.tsx`:
- Around line 151-155: The prop type mismatch occurs because me.profileImageUrl
is string | null but ProfileImageEditor.profileImageUrl expects string |
undefined; fix by normalizing the value before passing it (e.g., convert null to
undefined) or update ProfileImageEditor’s prop type to accept string | null;
locate the usage in EditProfileContent (ProfileImageEditor component call) and
either change the prop expression to pass undefined when me.profileImageUrl is
null or adjust the ProfileImageEditor prop type definition to string | null to
match me.profileImageUrl.
In `@src/components/mypage/edit/ProfileImageEditor.tsx`:
- Around line 26-31: The cleanup effect in ProfileImageEditor captures the
initial previewUrl (null) because it uses an empty dependency array, so when
unmounted the real object URL isn't revoked; fix this by introducing a ref
(e.g., previewUrlRef = useRef<string | null>(null)), ensure
previewUrlRef.current is updated whenever previewUrl changes (or at the point
you call setPreviewUrl), and change the unmount cleanup in the useEffect to call
URL.revokeObjectURL(previewUrlRef.current) if present; keep the unmount effect
without dependencies but read the latest URL from previewUrlRef.current to
safely revoke the object URL on unmount.
---
Outside diff comments:
In @.claude/settings.local.json:
- Around line 6-28: This file contains unresolved Git merge conflict markers
(<<<<<<< HEAD, =======, >>>>>>>) that break JSON; remove the conflict markers,
pick/merge the desired entries from the conflicting blocks (e.g., the different
"Bash(...)" lines and
"mcp__figma__get_design_context"/"mcp__figma__get_screenshot" entries), ensure
there are no duplicate or malformed entries, and produce a single valid JSON
array/object with proper commas and quoting so the file parses; pay special
attention to the nested conflict between the HEAD block and commits shown
(resolve which of the "Bash(find ...)" and "Bash(ls ...)" / "Bash(git fetch:*)"
/ "Bash(grep -r ...)" variants to keep) and remove any leftover whitespace or
stray characters from the merge markers.
🪄 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: 2e32433b-f899-4e91-9840-66282250ed56
📒 Files selected for processing (8)
.claude/settings.local.jsonsrc/components/mypage/edit/EditProfileContent.tsxsrc/components/mypage/edit/PersonalInfoFields.tsxsrc/components/mypage/edit/ProfileImageEditor.tsxsrc/hooks/mutations/mypage/useUpdateProfileMutation.tssrc/lib/apis/index.tssrc/lib/apis/server.tssrc/types/mypage.ts
🚧 Files skipped from review as they are similar to previous changes (5)
- src/lib/apis/server.ts
- src/hooks/mutations/mypage/useUpdateProfileMutation.ts
- src/components/mypage/edit/PersonalInfoFields.tsx
- src/lib/apis/index.ts
- src/types/mypage.ts
| useEffect(() => { | ||
| return () => { | ||
| if (previewUrl) URL.revokeObjectURL(previewUrl); | ||
| }; | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, []); |
There was a problem hiding this comment.
언마운트 시 Object URL이 해제되지 않습니다.
빈 의존성 배열 []로 인해 cleanup 함수가 마운트 시점의 previewUrl 값(null)을 캡처합니다. 컴포넌트가 언마운트될 때 실제 URL을 해제하지 못해 메모리 누수가 발생합니다.
ref를 사용하여 현재 URL을 추적하는 것이 안전합니다.
🛠️ ref를 사용한 수정 제안
- const [previewUrl, setPreviewUrl] = useState<string | null>(null);
+ const [previewUrl, setPreviewUrl] = useState<string | null>(null);
+ const previewUrlRef = useRef<string | null>(null);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
- if (previewUrl) URL.revokeObjectURL(previewUrl);
- setPreviewUrl(URL.createObjectURL(file));
+ if (previewUrlRef.current) URL.revokeObjectURL(previewUrlRef.current);
+ const newUrl = URL.createObjectURL(file);
+ previewUrlRef.current = newUrl;
+ setPreviewUrl(newUrl);
onFileChange?.(file);
};
useEffect(() => {
return () => {
- if (previewUrl) URL.revokeObjectURL(previewUrl);
+ if (previewUrlRef.current) URL.revokeObjectURL(previewUrlRef.current);
};
- // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/mypage/edit/ProfileImageEditor.tsx` around lines 26 - 31, The
cleanup effect in ProfileImageEditor captures the initial previewUrl (null)
because it uses an empty dependency array, so when unmounted the real object URL
isn't revoked; fix this by introducing a ref (e.g., previewUrlRef =
useRef<string | null>(null)), ensure previewUrlRef.current is updated whenever
previewUrl changes (or at the point you call setPreviewUrl), and change the
unmount cleanup in the useEffect to call
URL.revokeObjectURL(previewUrlRef.current) if present; keep the unmount effect
without dependencies but read the latest URL from previewUrlRef.current to
safely revoke the object URL on unmount.
🤖 Claude 테스트 제안
변경된 컴포넌트에 대해 Claude가 생성한 테스트 코드입니다. 검토 후 적합한 부분만 사용하세요.
|
PR 테스트 결과✅ Jest: 통과 🎉 모든 테스트를 통과했습니다! |
PR 검증 결과✅ TypeScript: 통과 |
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
src/components/admin/member/modal/MemberDetailModal.tsx (1)
155-168:⚠️ Potential issue | 🔴 Critical
handler가undefined일 때 런타임 에러 발생 가능
getFooterActions에서 반환되는handler는 선택적 속성(onChangeRole?,onBan?,onRestore?)에서 직접 할당되므로undefined일 수 있습니다. 현재 코드는.filter()없이 바로 매핑하여onClick={handler}를 전달하므로, 핸들러가 없는 상태에서 버튼 클릭 시undefined()가 호출되어 런타임 에러가 발생합니다.
filter를 다시 추가하거나handler가 정의된 경우에만 렌더링하도록 수정이 필요합니다.🔧 제안 수정안
- {footerActions.map(({ label, title, handler }) => ( + {footerActions + .filter(({ handler }) => handler !== undefined) + .map(({ label, title, handler }) => ( <AlertDialog key={label} title={title} trigger={ <Button variant="secondary" size="lg"> {label} </Button> } > <AlertDialogAction onClick={handler}>확인</AlertDialogAction> <AlertDialogCancel>취소</AlertDialogCancel> </AlertDialog> ))}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/admin/member/modal/MemberDetailModal.tsx` around lines 155 - 168, The mapping over footerActions passes potentially undefined handler to AlertDialogAction's onClick, causing runtime errors when handler is absent; update the render to only include actions with a defined handler (e.g., filter footerActions for entries where handler is a function or check handler !== undefined) before mapping, or conditionally render AlertDialog/AlertDialogAction only when handler exists; reference getFooterActions for where handlers originate and AlertDialogAction/handler in MemberDetailModal for the change.src/components/admin/member/MemberPageContent.tsx (1)
149-162:⚠️ Potential issue | 🟠 Major
LEAD역할이ADMIN으로 잘못 변경됨현재 로직은
memberRole === 'ADMIN'이 아닌 모든 경우를'ADMIN'으로 변경합니다.LEAD역할의 멤버가 "역할 변경" 버튼을 클릭하면 의도치 않게ADMIN으로 변경됩니다.
LEAD역할은 별도로 처리하거나 역할 변경을 차단해야 합니다.🔧 제안 수정안
onChangeRole={ detailMember ? () => { + if (detailMember.memberRole === 'LEAD') { + return; + } const nextRole = detailMember.memberRole === 'ADMIN' ? 'USER' : 'ADMIN';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/admin/member/MemberPageContent.tsx` around lines 149 - 162, The onChangeRole handler for detailMember incorrectly maps any non-ADMIN role (including LEAD) to 'ADMIN'; fix by explicitly handling roles: if detailMember.memberRole is 'ADMIN' set nextRole to 'USER', if it's 'USER' set nextRole to 'ADMIN', and if it's 'LEAD' either return/do nothing or show/block the change (i.e., do not call setDetailMember or changeMemberRole for 'LEAD'). Update the handler that sets nextRole and uses setDetailMember and changeMemberRole (referenced symbols: detailMember, onChangeRole, setDetailMember, changeMemberRole, ROLE_LABEL) so LEAD is preserved or handled separately.
🧹 Nitpick comments (2)
src/components/mypage/edit/EditProfileContent.tsx (1)
122-128: 하드코딩된 spacing 값을 디자인 토큰으로 대체하세요.
gap-[35px]와pb-[80px]는 하드코딩된 값입니다. 코딩 가이드라인에 따라 디자인 토큰 클래스를 우선 사용해야 합니다.🔧 제안 수정안
<div className={cn( - 'mx-auto flex w-full max-w-[1088px] flex-col gap-[35px] px-450 pt-450 pb-[80px]', + 'mx-auto flex w-full max-w-[1088px] flex-col gap-700 px-450 pt-450 pb-900', className, )} {...props} >적절한 토큰이 없다면 디자인 시스템에 새 토큰 추가가 필요한지 확인해주세요. 코딩 가이드라인에 따르면 "no hardcoded values; ask before adding new tokens"입니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/mypage/edit/EditProfileContent.tsx` around lines 122 - 128, EditProfileContent is using hardcoded spacing classes gap-[35px] and pb-[80px]; replace them with the appropriate design-token utility classes (e.g., the project's spacing tokens) inside the className on the root div in EditProfileContent; if suitable tokens don't exist, open a design-system request to add tokens matching 35px/80px and then update the className to use those new token classes, avoiding any remaining bracketed hardcoded values.src/components/admin/member/MemberPageContent.tsx (1)
72-81: 대량 뮤테이션 시 부분 실패 처리 및 UX 고려 필요
forEach로 여러 멤버에 대해 동시에 뮤테이션을 호출하면:
- 일부만 실패해도 사용자에게 명확한 피드백이 없음
- 로딩 상태 표시 없이 여러 API 호출이 병렬로 진행됨
현재 구현이 단순한 케이스에서는 동작하지만, 멤버 수가 많아지면 에러 핸들링과 로딩 상태 관리를 개선하는 것이 좋습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/admin/member/MemberPageContent.tsx` around lines 72 - 81, The current handlers (onChangeRole, onBan, onRestore) call changeMemberRole/banMember/restoreMember inside selectedMembers.forEach which fires many parallel mutations with no loading state or partial-failure handling; change each handler to run the per-member mutations using Promise.allSettled (or sequentially with for..of + await if rate-limiting is desired) over selectedMembers.map(m => changeMemberRole({ clubMemberId: m.clubMemberId, memberRole: targetRole })) (and similarly for banMember/restoreMember), set and expose a loading state while requests are in-flight, collect successes and failures from the results, and surface a summarized UX message (e.g., X succeeded, Y failed with error details) so partial failures are visible to the user.
🤖 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/MemberPageContent.tsx`:
- Around line 133-148: The local detailMember is being optimistically updated
before calling banMember/restoreMember, but those mutations may fail and React
Query's rollback doesn't revert this component state; update the component to
either perform the UI update inside the mutation onSuccess handlers or capture
the previousDetail (e.g., prev = detailMember) before optimistic set and revert
it in the mutation onError/onSettled callbacks; specifically modify the calls
around setDetailMember, banMember, and restoreMember so that setDetailMember({
...detailMember, status: 'BANNED' }) / setDetailMember({ ...detailMember,
status: 'ACTIVE' }) is moved into the corresponding mutation onSuccess or add
logic to restore prev via the mutation's onError for both banMember and
restoreMember.
In `@src/components/admin/member/modal/MemberDetailModal.tsx`:
- Around line 106-108: member.generation is parsed with parseInt in
MemberDetailModal which can produce NaN and render "NaN기"; guard against invalid
values by validating the parsed number (e.g., use Number.isFinite or isNaN
checks) and fall back to a safe display (empty string, "-" or "Unknown") when
parseInt(member.generation, 10) yields NaN; update the rendering logic around
parseInt(member.generation, 10) to compute a safeGeneration variable and use
that in the JSX.
---
Duplicate comments:
In `@src/components/admin/member/MemberPageContent.tsx`:
- Around line 149-162: The onChangeRole handler for detailMember incorrectly
maps any non-ADMIN role (including LEAD) to 'ADMIN'; fix by explicitly handling
roles: if detailMember.memberRole is 'ADMIN' set nextRole to 'USER', if it's
'USER' set nextRole to 'ADMIN', and if it's 'LEAD' either return/do nothing or
show/block the change (i.e., do not call setDetailMember or changeMemberRole for
'LEAD'). Update the handler that sets nextRole and uses setDetailMember and
changeMemberRole (referenced symbols: detailMember, onChangeRole,
setDetailMember, changeMemberRole, ROLE_LABEL) so LEAD is preserved or handled
separately.
In `@src/components/admin/member/modal/MemberDetailModal.tsx`:
- Around line 155-168: The mapping over footerActions passes potentially
undefined handler to AlertDialogAction's onClick, causing runtime errors when
handler is absent; update the render to only include actions with a defined
handler (e.g., filter footerActions for entries where handler is a function or
check handler !== undefined) before mapping, or conditionally render
AlertDialog/AlertDialogAction only when handler exists; reference
getFooterActions for where handlers originate and AlertDialogAction/handler in
MemberDetailModal for the change.
---
Nitpick comments:
In `@src/components/admin/member/MemberPageContent.tsx`:
- Around line 72-81: The current handlers (onChangeRole, onBan, onRestore) call
changeMemberRole/banMember/restoreMember inside selectedMembers.forEach which
fires many parallel mutations with no loading state or partial-failure handling;
change each handler to run the per-member mutations using Promise.allSettled (or
sequentially with for..of + await if rate-limiting is desired) over
selectedMembers.map(m => changeMemberRole({ clubMemberId: m.clubMemberId,
memberRole: targetRole })) (and similarly for banMember/restoreMember), set and
expose a loading state while requests are in-flight, collect successes and
failures from the results, and surface a summarized UX message (e.g., X
succeeded, Y failed with error details) so partial failures are visible to the
user.
In `@src/components/mypage/edit/EditProfileContent.tsx`:
- Around line 122-128: EditProfileContent is using hardcoded spacing classes
gap-[35px] and pb-[80px]; replace them with the appropriate design-token utility
classes (e.g., the project's spacing tokens) inside the className on the root
div in EditProfileContent; if suitable tokens don't exist, open a design-system
request to add tokens matching 35px/80px and then update the className to use
those new token classes, avoiding any remaining bracketed hardcoded values.
🪄 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: bc35fffd-3ef0-468e-b848-1db3846bc51a
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (7)
src/components/admin/member/MemberPageContent.tsxsrc/components/admin/member/modal/MemberDetailModal.tsxsrc/components/mypage/MyPageContent.tsxsrc/components/mypage/edit/EditProfileContent.tsxsrc/hooks/mutations/admin/useAdminCardinalMutations.tssrc/hooks/mutations/admin/useAdminMemberMutations.tssrc/lib/apis/mypage.ts
✅ Files skipped from review due to trivial changes (1)
- src/lib/apis/mypage.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- src/hooks/mutations/admin/useAdminCardinalMutations.ts
- src/components/mypage/MyPageContent.tsx
- src/hooks/mutations/admin/useAdminMemberMutations.ts
| onBan={ | ||
| detailMember | ||
| ? () => { | ||
| setDetailMember({ ...detailMember, status: 'BANNED' }); | ||
| banMember(detailMember.clubMemberId); | ||
| } | ||
| : undefined | ||
| } | ||
| onRestore={ | ||
| detailMember | ||
| ? () => { | ||
| setDetailMember({ ...detailMember, status: 'ACTIVE' }); | ||
| restoreMember(detailMember.clubMemberId); | ||
| } | ||
| : undefined | ||
| } |
There was a problem hiding this comment.
뮤테이션 실패 시 로컬 상태 롤백 누락
banMember와 restoreMember 호출 전에 setDetailMember로 낙관적 업데이트를 수행하고 있습니다. 뮤테이션 훅의 onError는 React Query 캐시만 롤백하므로, 실패 시 detailMember 로컬 상태는 잘못된 상태로 남아 모달 UI와 서버 데이터가 불일치하게 됩니다.
뮤테이션의 onError 또는 onSettled 콜백에서 로컬 상태도 동기화하거나, 낙관적 업데이트 대신 onSuccess에서 상태를 업데이트하는 방식을 고려하세요.
🔧 onSuccess 기반 업데이트 예시
onBan={
detailMember
? () => {
- setDetailMember({ ...detailMember, status: 'BANNED' });
- banMember(detailMember.clubMemberId);
+ banMember(detailMember.clubMemberId, {
+ onSuccess: () => setDetailMember({ ...detailMember, status: 'BANNED' }),
+ });
}
: undefined
}
onRestore={
detailMember
? () => {
- setDetailMember({ ...detailMember, status: 'ACTIVE' });
- restoreMember(detailMember.clubMemberId);
+ restoreMember(detailMember.clubMemberId, {
+ onSuccess: () => setDetailMember({ ...detailMember, status: 'ACTIVE' }),
+ });
}
: undefined
}📝 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.
| onBan={ | |
| detailMember | |
| ? () => { | |
| setDetailMember({ ...detailMember, status: 'BANNED' }); | |
| banMember(detailMember.clubMemberId); | |
| } | |
| : undefined | |
| } | |
| onRestore={ | |
| detailMember | |
| ? () => { | |
| setDetailMember({ ...detailMember, status: 'ACTIVE' }); | |
| restoreMember(detailMember.clubMemberId); | |
| } | |
| : undefined | |
| } | |
| onBan={ | |
| detailMember | |
| ? () => { | |
| banMember(detailMember.clubMemberId, { | |
| onSuccess: () => setDetailMember({ ...detailMember, status: 'BANNED' }), | |
| }); | |
| } | |
| : undefined | |
| } | |
| onRestore={ | |
| detailMember | |
| ? () => { | |
| restoreMember(detailMember.clubMemberId, { | |
| onSuccess: () => setDetailMember({ ...detailMember, status: 'ACTIVE' }), | |
| }); | |
| } | |
| : undefined | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/admin/member/MemberPageContent.tsx` around lines 133 - 148,
The local detailMember is being optimistically updated before calling
banMember/restoreMember, but those mutations may fail and React Query's rollback
doesn't revert this component state; update the component to either perform the
UI update inside the mutation onSuccess handlers or capture the previousDetail
(e.g., prev = detailMember) before optimistic set and revert it in the mutation
onError/onSettled callbacks; specifically modify the calls around
setDetailMember, banMember, and restoreMember so that setDetailMember({
...detailMember, status: 'BANNED' }) / setDetailMember({ ...detailMember,
status: 'ACTIVE' }) is moved into the corresponding mutation onSuccess or add
logic to restore prev via the mutation's onError for both banMember and
restoreMember.
| <span className="typo-h3 text-text-strong"> | ||
| {parseInt(member.generation, 10)}기 | ||
| </span> |
There was a problem hiding this comment.
parseInt 결과가 NaN이 될 수 있음
member.generation이 숫자로 변환할 수 없는 문자열인 경우 NaN기로 표시됩니다. 서버 데이터가 항상 유효한 숫자 문자열임이 보장된다면 문제없으나, 방어적으로 처리하는 것이 안전합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/admin/member/modal/MemberDetailModal.tsx` around lines 106 -
108, member.generation is parsed with parseInt in MemberDetailModal which can
produce NaN and render "NaN기"; guard against invalid values by validating the
parsed number (e.g., use Number.isFinite or isNaN checks) and fall back to a
safe display (empty string, "-" or "Unknown") when parseInt(member.generation,
10) yields NaN; update the rendering logic around parseInt(member.generation,
10) to compute a safeGeneration variable and use that in the JSX.
useAdminMemberMutations에서 @/lib/apis barrel import 대신 @/lib/apis/adminMember 직접 import로 변경하여 클라이언트 번들에 next/headers가 포함되는 문제 해결 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PR 테스트 결과✅ Jest: 통과 🎉 모든 테스트를 통과했습니다! |
🤖 Claude 테스트 제안
변경된 컴포넌트에 대해 Claude가 생성한 테스트 코드입니다. 검토 후 적합한 부분만 사용하세요.
|
PR 검증 결과✅ TypeScript: 통과 🎉 모든 검증을 통과했습니다! |
|
구현한 기능 Preview: https://weeth-azy55xrgv-weethsite-4975s-projects.vercel.app |
woneeeee
left a comment
There was a problem hiding this comment.
수고하셨습니당!!! 드디어 유저 부분은 끝이 보이네요... 하핫
There was a problem hiding this comment.
엇 뭐지 이 부분은.... 충돌이 해결이 안된 파일인 것 같아요ㅜ
| useEffect(() => { | ||
| return () => { | ||
| if (previewUrl) URL.revokeObjectURL(previewUrl); | ||
| }; | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps |
There was a problem hiding this comment.
요 부분 eslint-disable 처리하지말고 previewUrlRef로 해서 useRef로 최신값 추적하게 하는건 어떨까욤?
| <p className="typo-body1 text-state-error">내 정보를 불러올 수 없습니다.</p> | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
이 부분이 src/components/mypage/edit/EditProfileContent.tsx 여기에도 있었는데 useMyMemberQuery쿼리에서 적용해둔건 어떨까욤?
| @@ -102,9 +35,7 @@ export const SORT_LABEL: Record<SortBy, string> = { | |||
| export function sortMembers(members: Member[], sortBy: SortBy): Member[] { | |||
There was a problem hiding this comment.
여기sortMembers는 유틸함수인데 상수 파일에 들어와있는 것 같슴니닷!!
| profileImageFile?: File | null; | ||
| } | ||
|
|
||
| async function uploadProfileImage(file: File) { |
There was a problem hiding this comment.
여기서만 사용되는건 아닌 것 같응ㄴ데 업로드 이미지 부분도 분리하는건 어떨까욤...?
| interface SchoolsResponse { | ||
| code: number; | ||
| message: string; | ||
| data: School[]; | ||
| } | ||
|
|
||
| interface MajorsResponse { | ||
| code: number; | ||
| message: string; | ||
| data: Major[]; | ||
| } |
There was a problem hiding this comment.
요거 공통 ApiResponse 사용해도 좋을 것 같아욤
| USER: '사용자', | ||
| ADMIN: '관리자', | ||
| LEAD: '리더', | ||
| }; |
There was a problem hiding this comment.
이거 src/utils/admin/memberMapper.ts에 그대로 똑같이 있어서 그거 사용해줘도 될 것 같아욤
nabbang6
left a comment
There was a problem hiding this comment.
고생하셧습니다 !!!!
쿼리랑 뮤테이션 분리해두니까 깔끔하고 좋네용... 저도 앞으로 작업하면서 반영해두겟습니닷 🥕
| await mypageApi.updateUser(user); | ||
|
|
||
| const profileImage = profileImageFile | ||
| ? await uploadProfileImage(profileImageFile) | ||
| : undefined; | ||
|
|
||
| await mypageApi.updateClubProfile({ | ||
| bio: clubProfile.bio, | ||
| ...(profileImage && { profileImage }), | ||
| }); | ||
| }, |
There was a problem hiding this comment.
요기 세 api가 순차적으로 호출되고 잇는 것 같은데, updateUser, updateClubProfile은 병렬 처리 해주면 어떨까 싶습니다!
단 profileImage 업로드는 updateClubProfile에 필요하니까 고것만 먼저 처리해줌 좋을 것 같아용
const [, profileImage] = await Promise.all([
mypageApi.updateUser(user),
profileImageFile ? uploadProfileImage(profileImageFile) : undefined,
]);
await mypageApi.updateClubProfile({
bio: clubProfile.bio,
...(profileImage && { profileImage }),
});
이런 느낌으로,,
| const { | ||
| register, | ||
| handleSubmit, | ||
| setValue, | ||
| reset, | ||
| control, | ||
| formState: { errors }, | ||
| } = useForm<EditProfileFormData>({ | ||
| resolver: zodResolver(editProfileSchema), | ||
| mode: 'onBlur', | ||
| defaultValues: { | ||
| name: '', | ||
| bio: '', | ||
| tel: '', | ||
| email: '', | ||
| school: '', | ||
| department: '', | ||
| studentId: '', | ||
| }, | ||
| }); | ||
|
|
||
| useEffect(() => { | ||
| if (me) { | ||
| reset( | ||
| { | ||
| name: me.name, | ||
| bio: me.bio ?? '', | ||
| tel: me.tel ? formatPhone(me.tel) : '', | ||
| email: me.email, | ||
| school: me.school, | ||
| department: me.department, | ||
| studentId: me.studentId, | ||
| }, | ||
| { keepDirtyValues: true }, | ||
| ); | ||
| } | ||
| }, [me, reset]); |
There was a problem hiding this comment.
요기 useEffect로 me 데이터를 reset 해주고 잇는 것 같은데, defaultValues를 비동기로 설정해주면 effect 없이 처리해줄 수 잇을 것 같습니다!
const { ... } = useForm({
resolver: zodResolver(editProfileSchema),
mode: 'onBlur',
values: me ? {
name: me.name,
bio: me.bio ?? '',
tel: me.tel ? formatPhone(me.tel) : '',
email: me.email,
school: me.school,
department: me.department,
studentId: me.studentId,
} : undefined,
});
React Hook Form values 옵션이 자동으로 폼 동기화를 시켜준다고 하네용
| export function useCardinals() { | ||
| const clubId = useClubId(); | ||
|
|
||
| return useQuery({ | ||
| queryKey: ['cardinals', clubId], | ||
| queryFn: async () => { | ||
| const res = await cardinalApi.getCardinals(clubId!); | ||
| const data = res.data.data; | ||
| return data; | ||
| }, | ||
| enabled: !!clubId, | ||
| staleTime: 30 * 60 * 1000, | ||
| gcTime: 60 * 60 * 1000, | ||
| }); | ||
| } |
There was a problem hiding this comment.
별건 아니지만,,, ㅎ__ㅎ useCardinals도 다른 query 훅들처럼 useCardinalsQuery로 통일해줘도 좋을 것 같습니다!
📢 어드민 멤버 페이지 땡겨오느라 변경 사항이 저래 됐습니다.. 마이페이지만 봐주십시오..
✅ PR 유형
어떤 변경 사항이 있었나요?
📌 관련 이슈번호
✅ Key Changes
GET /clubs/{clubId}/members/me) — MOCK 데이터 제거, 실제 API 데이터로 교체GET /clubs) — 활동정보 섹션에 실제 클럽 데이터 표시PATCH /users+PATCH /clubs/members/me)onTouched모드)URL.createObjectURL미리보기POST /clubs/{clubId}/members/me/cardinals)useCardinals쿼리로 기수 목록 조회하여 SetCardinalModal에 전달edit/폴더ProfileImageEditor— 아바타 + 이미지 편집PersonalInfoFields— 이름, 소개글, 연락처, 이메일SchoolInfoFields— 학교, 학과, 학번SupportListItem텍스트 왼쪽 정렬 (text-left)MyPageDropdownMenu탈퇴 문구 서비스 탈퇴로 변경, separator 중복 제거ClubInfoCard에서availableCardinalsprop 제거 (내부에서 API 조회)📸 스크린샷 or 실행영상
🎸 기타 사항 or 추가 코멘트
훅 폴더 뮤테이션이랑 쿼리 분리 해놧는데 앞으로 작업하실 때 분리해서 해주시믄 짱 좋을 듯... 뮤테이션이랑 쿼리 안에 폴더나 파일 알잘딱 넣어서 써주심 됨니다
🤖 Generated with Claude Code
Summary by CodeRabbit
릴리스 노트
New Features
Improvements
Chores