Conversation
Walkthrough관리자 페이지 리디자인 반영: 공통 컴포넌트에 신규 props 추가(BottomModal.overlayClassName, ToggleSwitch의 ariaLabel/labelClassName/variant) 및 manager 전용 헤더(ManagerHeader) 도입. 헤더 타입·매칭 로직과 관련 상수(MANAGER_HEADER_HEIGHT) 확장. 여러 Manager 페이지들(계정/지원서/동아리/회원 관리 등)에서 UI 구조, 폼/이미지 처리, 모달 흐름, 목록 페이징(무한스크롤) 및 훅(관리용 애플리케이션/멤버 훅)들을 대대적으로 리팩터링/기능 확장함. Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
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. Comment |
There was a problem hiding this comment.
Pull request overview
관리자(동아리 관리) 영역의 화면/컴포넌트들을 신규 디자인에 맞게 전반 리팩터링하고, 지원서 상세 UI를 공통 컴포넌트로 통합하는 PR입니다.
Changes:
- 관리자 전용 헤더 타입(
manager) 추가 및/mypage/manager하위 라우트 타이틀/레이아웃 패딩 조정 - 지원서 상세 화면 UI를
ApplicationDetailContent로 공통화하고, 지원자 목록은 무한 스크롤 기반으로 개편 - 모집 공고/지원서 문항/회비/부원 관리 등 관리자 주요 화면 UI 리디자인 및 상호작용 개선
Reviewed changes
Copilot reviewed 23 out of 34 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/pages/User/MyPage/components/UserInfoCard.tsx | 마이페이지 카드/관리자 카드 UI 리디자인 및 레이아웃 조정 |
| src/pages/Manager/hooks/useManagedMembers.ts | 직책 변경 mutation에 옵션(토스트/무효화) 지원 추가 |
| src/pages/Manager/hooks/useManagedApplications.ts | 지원자 목록을 suspense infinite query 기반으로 전환 및 query key 재정비 |
| src/pages/Manager/components/ApplicationDetailContent.tsx | 지원서 상세 UI 공통 컴포넌트 신규 추가 |
| src/pages/Manager/ManagedRecruitmentWrite/index.tsx | 모집 공고 작성/수정 UI 리디자인 및 설정 토글 연동 |
| src/pages/Manager/ManagedRecruitmentForm/index.tsx | 지원서 문항 편집 UI 리디자인 및 설정 토글 연동 |
| src/pages/Manager/ManagedRecruitment/index.tsx | 모집/지원서/회비 진입 화면을 신규 카드 UI로 재구성 |
| src/pages/Manager/ManagedMemberList/index.tsx | 부원 관리 화면 리디자인(액션 메뉴/직책 변경 모달 등) |
| src/pages/Manager/ManagedMemberApplicationDetail/index.tsx | 지원서 상세 공통 컴포넌트로 대체 |
| src/pages/Manager/ManagedClubProfile/index.tsx | 동아리 정보 수정 UI 리디자인 및 로컬 프리뷰 URL 정리 강화 |
| src/pages/Manager/ManagedClubList/index.tsx | 관리자 진입(동아리 목록) UI 리디자인 |
| src/pages/Manager/ManagedClubDetail/index.tsx | 동아리 상세(메뉴) UI 리디자인 |
| src/pages/Manager/ManagedApplicationList/index.tsx | 지원자 목록 UI 리디자인 + 무한 스크롤 적용 |
| src/pages/Manager/ManagedApplicationDetail/index.tsx | 지원서 상세 공통 컴포넌트로 대체 + 승인/거절 footer 구성 |
| src/pages/Manager/ManagedAccount/index.tsx | 회비 설정 UI 리디자인 + 활성화 토글/은행 선택 모달 개선 |
| src/components/layout/index.tsx | manager 헤더일 때 main 패딩 상단 값 분기 추가 |
| src/components/layout/Header/types.ts | HeaderType에 manager 추가 |
| src/components/layout/Header/routeTitles.ts | /mypage/manager 하위 라우트 타이틀 매핑 추가 |
| src/components/layout/Header/index.tsx | manager 타입 렌더러로 ManagerHeader 연결 |
| src/components/layout/Header/headerConfig.ts | /mypage/manager 경로를 manager 헤더 타입으로 매칭 |
| src/components/layout/Header/components/ManagerHeader.tsx | 관리자 전용 헤더 신규 추가(스마트 백 + 알림) |
| src/components/common/ToggleSwitch.tsx | 토글에 manager variant/ariaLabel/labelClassName 확장 |
| src/components/common/BottomModal.tsx | 오버레이 스타일 커스터마이즈를 위한 overlayClassName 추가 |
| src/assets/svg/role-selector-arrow-down.svg | 역할 선택 UI용 아이콘 추가 |
| src/assets/svg/more-horizontal.svg | 액션(더보기) 아이콘 추가 |
| src/assets/svg/close.svg | 닫기 아이콘 추가 |
| src/assets/svg/add-photo-alternate.svg | 이미지 추가 아이콘 추가 |
| src/assets/svg/Chevron-left-dark.svg | 신규 chevron 아이콘 추가(다크) |
| src/assets/image/folder.png | 관리자 메뉴용 3D 이미지 추가 |
| src/assets/image/chat.png | 관리자 메뉴용 3D 이미지 추가 |
| src/assets/image/boy.png | 관리자 메뉴용 3D 이미지 추가 |
| src/assets/image/3d-flag.png | 관리자 메뉴/상태 카드용 3D 이미지 추가 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| isManager | ||
| ? 'relative h-5 w-[37px] rounded-full transition-colors focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-60' | ||
| : 'relative touch-manipulation rounded-full transition-colors focus-visible:ring-2 focus-visible:ring-indigo-300 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-60', | ||
| isManager | ||
| ? enabled | ||
| ? 'bg-primary-500' | ||
| : 'bg-text-100' | ||
| : isHorizontal | ||
| ? `h-7 w-12 border border-indigo-50 ${enabled ? 'bg-indigo-700' : 'bg-indigo-50'}` | ||
| : `h-5 w-9 ${enabled ? 'bg-primary' : 'bg-indigo-100'}` |
| <div | ||
| className="border-indigo-5 active:bg-indigo-5/60 flex cursor-pointer items-center justify-between rounded-2xl border bg-white p-3" | ||
| onClick={() => onDetail(application.id)} | ||
| > |
| <img className="h-12 w-12 rounded" src={managedClub?.imageUrl} alt={`${currentClub?.name} 동아리 사진`} /> | ||
| <div> | ||
| <div className={cn('text-h2 font-bold text-indigo-700')}>{currentClub?.name} 관리자</div> | ||
| <div className="mt-1.5 text-xs leading-3.5 font-medium text-indigo-300"> | ||
| {myInfo.studentNumber} · {myInfo.universityName} · {currentClub?.position} | ||
| <div className="text-[16px] leading-[1.6] font-bold text-indigo-700">{currentClub?.name} 정보</div> | ||
| <div className="text-[11px] leading-[15px] font-medium text-indigo-300"> | ||
| {myInfo.studentNumber} / {myInfo.universityName} / {currentClub?.position} |
| const cardClassName = cn( | ||
| 'rounded-2xl bg-white p-4 shadow-[0_0_3px_0_rgba(0,0,0,0.2)]', | ||
| isClickable && 'cursor-pointer active:bg-indigo-5/50' | ||
| ); | ||
|
|
||
| return ( | ||
| <Card {...cardProps}> | ||
| <div className={cardClassName} onClick={isClickable ? handleCardClick : undefined}> | ||
| <div className="flex items-center justify-between gap-3"> |
There was a problem hiding this comment.
Actionable comments posted: 16
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/pages/Manager/ManagedClubProfile/index.tsx (1)
107-126:⚠️ Potential issue | 🟡 Minor업로드 플래그가 전체 제출 상태로도 쓰여서 로딩 문구가 틀립니다.
이미지를 바꾸지 않아도 제출 시작 시
isUploading이true가 되어 메인 버튼이이미지 업로드 중...으로 보입니다. 업로드 구간에서만 켜거나 제출 상태를 분리하는 게 맞습니다.수정 예시
const handleSubmit = async () => { closeSubmitModal(); - setIsUploading(true); try { let finalImageUrl = imagePreview; if (imageFile) { + setIsUploading(true); const result = await uploadImage(imageFile); finalImageUrl = result.fileUrl; + setIsUploading(false); } await updateClubInfo({ description, imageUrl: finalImageUrl, location, introduce, }); } finally { setIsUploading(false); } };Also applies to: 244-250
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedClubProfile/index.tsx` around lines 107 - 126, The submit flow currently uses isUploading for the whole submission so the main button shows "이미지 업로드 중..." even when no upload occurs; update handleSubmit to only toggle isUploading around the uploadImage call (setIsUploading(true) immediately before uploadImage and setIsUploading(false) right after its result) and introduce a separate submitting flag (e.g., isSubmitting with setIsSubmitting) to represent the overall form submission around updateClubInfo; adjust the UI text checks to use isUploading for image-only status and isSubmitting for overall submit state, and apply the same change to the other handler referenced (the code at the second occurrence).
🧹 Nitpick comments (22)
src/pages/Manager/hooks/useManagedApplications.ts (2)
120-127: Mutation에 에러 핸들링 추가 권장.
onError콜백이 없어서 API 실패 시 사용자에게 피드백이 없습니다. 에러 토스트를 추가하면 UX가 개선됩니다.♻️ 에러 핸들링 추가 예시
return useMutation({ mutationFn: (applicationId: number) => postClubApplicationApprove(clubId, applicationId), onSuccess: () => { showToast('지원이 승인되었습니다'); queryClient.invalidateQueries({ queryKey: applicationQueryKeys.managedClubApplications(clubId) }); if (navigateBack) navigate(-1); }, + onError: () => { + showToast('승인에 실패했습니다'); + }, });As per coding guidelines: "에러 핸들링(onError, throwOnError)이 적절히 처리되는지"
Also applies to: 136-143
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/hooks/useManagedApplications.ts` around lines 120 - 127, The mutation returned by useManagedApplications (the useMutation call using postClubApplicationApprove and queryKey applicationQueryKeys.managedClubApplications) lacks an onError handler; add an onError callback to the useMutation that calls showToast with a meaningful error message (and any returned API error details) and optionally logs the error, and ensure you mirror the same onError addition for the other mutation block (the one at lines 136-143) so both approve and reject flows report failures to the user; keep existing onSuccess behavior (invalidateQueries and navigate back) unchanged.
71-72:managedClubApplicationList네이밍 검토 필요.
managedClubApplicationList는 첫 페이지의 전체 응답 객체(currentPage, totalPage 등 포함)인데, 이름이 단순 리스트처럼 보입니다.firstPageResponse나paginationInfo처럼 명확한 이름을 고려해보세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/hooks/useManagedApplications.ts` around lines 71 - 72, Rename the misleading variable managedClubApplicationList to a clearer name that reflects it holds the first page response (e.g., firstPageResponse or firstPageData) and update all references where it's used; specifically change the declaration const managedClubApplicationList = data.pages[0] ?? null to use the new identifier and ensure any code that relied on managedClubApplicationList (such as accessing currentPage, totalPage, etc.) is updated to the new name, while keeping applications = data.pages.flatMap((page) => page?.applications ?? []) unchanged.src/pages/Manager/ManagedApplicationDetail/index.tsx (3)
10-10: 상대 경로 대신@/*경로 별칭 사용 권장코딩 가이드라인에 따라
../hooks/를@/pages/Manager/hooks/로 변경하는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedApplicationDetail/index.tsx` at line 10, 해당 파일의 import 구문에서 상대경로 '../hooks/useManagedApplications' 대신 프로젝트 경로 별칭을 사용하도록 변경하세요; 구체적으로 ManagedApplicationDetail 컴포넌트에서 사용된 useManagedApplications import를 '@/pages/Manager/hooks/useManagedApplications' 같은 별칭 경로로 교체하도록 업데이트하면 됩니다.
74-90: 반복되는 버튼 스타일 추출 고려승인/거절/취소 버튼의 className이 거의 동일하게 반복됩니다. 공통 버튼 컴포넌트나 스타일 상수로 추출하면 유지보수성이 향상됩니다.
Also applies to: 98-114
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedApplicationDetail/index.tsx` around lines 74 - 90, The three repeated button elements (the Cancel button using closeApprove and the Approve button using handleApprove, observing isPending and isApproving) duplicate almost identical className strings; extract a shared Button component or a constant for the shared className and use props to vary onClick, disabled, background color or label (e.g., create ManagedActionButton or BUTTON_BASE_CLASS and reuse it in the Cancel/Approve/Reject locations, passing isPending/isApproving to control disabled/label and preserving existing handlers closeApprove and handleApprove).
49-66: 하드코딩된 색상 값 디자인 토큰으로 대체 권장
#69BFDF색상이 버튼 스타일에 반복적으로 사용됩니다.theme.css의 색상 토큰(예:blue-400,primary등)으로 대체하거나, 자주 사용되는 버튼이라면 공통 버튼 컴포넌트로 추출하는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedApplicationDetail/index.tsx` around lines 49 - 66, Replace hardcoded "#69BFDF" color usages in the two buttons inside the ManagedApplicationDetail component with the design tokens from theme.css (e.g., use classes like "bg-primary", "border-primary", "text-primary" or "blue-400" equivalents) and/or refactor these buttons to use the shared Button component if one exists; update the JSX where openReject/openApprove are used (and the className strings referencing the hex) to apply the token classes and ensure disabled styles still use disabled:opacity-50 and disabled:cursor-not-allowed while preserving conditional labels driven by isRejecting/isApproving and the disabled prop bound to isPending.src/pages/Manager/components/ApplicationDetailContent.tsx (3)
113-124: 이미지 모달 접근성 개선 필요현재 백드롭 클릭으로만 모달 닫기가 가능합니다. 키보드 사용자를 위해
Escape키로 모달을 닫을 수 있도록onKeyDown핸들러 추가를 권장합니다.♻️ Escape 키 핸들링 예시
+import { useEffect } from 'react'; function ApplicationDetailContent({ application, footer }: ApplicationDetailContentProps) { const { value: isImageOpen, setTrue: openImage, setFalse: closeImage } = useBooleanState(); + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape' && isImageOpen) closeImage(); + }; + document.addEventListener('keydown', handleEscape); + return () => document.removeEventListener('keydown', handleEscape); + }, [isImageOpen, closeImage]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/components/ApplicationDetailContent.tsx` around lines 113 - 124, The image modal only closes via backdrop click; add keyboard handling so pressing Escape also closes it: when rendering the Portal-backed backdrop div (the element using isImageOpen and application.feePaymentImageUrl), attach an onKeyDown handler that calls closeImage when event.key === 'Escape', make that div focusable (e.g., tabIndex={-1}) and move focus to it when isImageOpen becomes true (or register a document keydown listener in a useEffect that cleans up) to ensure the Escape key is caught; keep existing stopPropagation on the <img> and ensure the key handler is removed when the modal closes.
17-18: 시맨틱 타이포그래피 토큰 사용 권장
text-[20px],text-[15px],text-[13px]등 커스텀 폰트 사이즈 대신text-body1,text-body2,text-sub1등 시맨틱 타이포그래피 유틸리티를 우선 사용해 주세요.Also applies to: 21-22, 24-25, 42-42, 62-63, 65-65, 67-68, 97-98
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/components/ApplicationDetailContent.tsx` around lines 17 - 18, Replace hardcoded font-size utilities (e.g. text-[20px], text-[15px], text-[13px]) in ApplicationDetailContent.tsx with the project's semantic typography tokens (e.g. text-body1, text-body2, text-sub1). Locate the JSX elements that render the avatar initial (the div using name.charAt(0)) and the other text spans in this component (the other occurrences referenced in the review) and swap each custom size class for the appropriate semantic class to preserve visual hierarchy (20px→text-body1, 15px→text-body2, 13px→text-sub1 or equivalent token per your design system). Ensure you update every listed instance in the file so styling is consistent and no custom text-[...] classes remain.
15-29: 하드코딩된 색상 값 대신 디자인 토큰 사용 권장
#F4F6F9,#E7EBEF,#5A6B7F등 하드코딩된 색상이 다수 사용되고 있습니다. 코딩 가이드라인에 따라src/styles/theme.css의 색상 토큰(indigo-, blue- 등)을 우선 사용해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/components/ApplicationDetailContent.tsx` around lines 15 - 29, This component (ApplicationDetailContent.tsx) uses hard-coded hex colors in className strings (e.g., border-[`#F4F6F9`], bg-[`#E7EBEF`], text-[`#5A6B7F`]); replace those hex values with the corresponding design tokens from src/styles/theme.css (use the appropriate indigo-*, blue-*, or text-* tokens) inside the same className attributes (for example swap border-[`#F4F6F9`] to the matching border token, bg-[`#E7EBEF`] to the matching bg token, and text-[`#5A6B7F`] to the matching text token) so the visual styles rely on the centralized theme rather than hard-coded hex values.src/pages/Manager/ManagedMemberApplicationDetail/index.tsx (1)
3-3: 상대 경로 대신@/*경로 별칭 사용 권장코딩 가이드라인에 따라
../hooks/를@/pages/Manager/hooks/로 변경하는 것을 권장합니다.♻️ 경로 별칭 적용
-import { useGetManagedMemberApplicationDetailByUser } from '../hooks/useManagedApplications'; +import { useGetManagedMemberApplicationDetailByUser } from '@/pages/Manager/hooks/useManagedApplications';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedMemberApplicationDetail/index.tsx` at line 3, Update the import in ManagedMemberApplicationDetail's module to use the project path alias instead of a relative path: replace the relative import of useGetManagedMemberApplicationDetailByUser from '../hooks/useManagedApplications' with the aliased path '@/pages/Manager/hooks/useManagedApplications' (keep the imported symbol name useGetManagedMemberApplicationDetailByUser unchanged) so the file index.tsx uses the recommended `@/`* alias.src/pages/Manager/ManagedApplicationList/index.tsx (1)
47-52: Typography 토큰 사용 권장
text-[15px],text-[13px]대신 가이드라인에 정의된 시맨틱 타이포그래피 유틸리티(text-body1,text-sub1등)를 우선 고려해보세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedApplicationList/index.tsx` around lines 47 - 52, Replace hard-coded pixel typography classes with the project's semantic typography tokens: change the className containing "text-[15px] leading-6 font-semibold text-indigo-700" to use the semantic token (e.g., "text-body1") while preserving other utilities like leading, weight, and color, and change the className containing "text-[13px] leading-[1.6] font-medium text-indigo-300" to use the semantic token (e.g., "text-sub1"); update in the ManagedApplicationList JSX where these className strings are defined so the visual size maps to the guideline tokens (text-body1, text-sub1) instead of raw pixel values.src/pages/Manager/ManagedMemberList/index.tsx (7)
414-420: 순차await루프는 성능 저하 유발각 요청이 독립적이므로
Promise.all로 병렬 처리하면 응답 시간을 단축할 수 있습니다. 단, 부분 실패 시 일부만 적용되는 점은 현재와 동일합니다.♻️ 제안
- for (const userId of promoteUserIds) { - await changeMemberPosition({ userId, data: { position: 'MANAGER' } }); - } - - for (const userId of demoteUserIds) { - await changeMemberPosition({ userId, data: { position: 'MEMBER' } }); - } + await Promise.all([ + ...promoteUserIds.map((userId) => changeMemberPosition({ userId, data: { position: 'MANAGER' } })), + ...demoteUserIds.map((userId) => changeMemberPosition({ userId, data: { position: 'MEMBER' } })), + ]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedMemberList/index.tsx` around lines 414 - 420, The loops over promoteUserIds and demoteUserIds currently await each changeMemberPosition call sequentially, causing slower overall execution; change them to run in parallel by mapping each id to changeMemberPosition and awaiting Promise.all for each group (i.e., await Promise.all(promoteUserIds.map(id => changeMemberPosition(...))) and similarly for demoteUserIds) so requests are issued concurrently while preserving the overall behavior; reference the promoteUserIds, demoteUserIds arrays and the changeMemberPosition function when making the change.
91-93: 조건부 클래스에cn()유틸리티 사용 권장코딩 가이드라인에 따라 Tailwind 클래스 병합 시
cn()유틸리티를 사용해야 합니다.♻️ 제안
+import { cn } from '@/utils/ts/cn'; - className={`text-sub2 text-text-600 w-full px-3 py-[3px] text-left ${ - index !== ROLE_OPTIONS.length - 1 ? 'border-b border-[`#C6CFD8`]' : '' - }`} + className={cn( + 'text-sub2 text-text-600 w-full px-3 py-[3px] text-left', + index !== ROLE_OPTIONS.length - 1 && 'border-b border-[`#C6CFD8`]' + )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedMemberList/index.tsx` around lines 91 - 93, Replace the conditional template literal used in the className prop inside the ManagedMemberList JSX with the cn() utility to merge Tailwind classes; specifically update the className that references index and ROLE_OPTIONS so it calls cn('text-sub2 text-text-600 w-full px-3 py-[3px] text-left', { 'border-b border-[`#C6CFD8`]': index !== ROLE_OPTIONS.length - 1 }) (or equivalent object/array form) to follow the project's Tailwind class merging guideline and ensure consistent class composition.
222-268: 페이지 로직을 커스텀 훅으로 분리 권장코딩 가이드라인에 따르면 페이지별 비즈니스 로직은
hooks/디렉토리로 분리해야 합니다. 현재 상태 관리, 핸들러, 뮤테이션 로직이 모두 컴포넌트 내에 있어 가독성과 테스트가 어렵습니다.예:
useManagedMemberListPage훅으로 상태와 핸들러를 추출As per coding guidelines: "Separate page logic into page-specific
hooks/subdirectory instead of keeping it in the page component"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedMemberList/index.tsx` around lines 222 - 268, The page component ManagedMemberList currently contains business logic and state (calls to useManagedMembers, useTransferPresident, useChangeVicePresident, useChangeMemberPosition, useRemoveMember, useAddPreMember, useGetPreMemberList, useDeletePreMember plus state like selectedMember, selectedPreMember, isRoleManageOpen, isActionOpen, roleManageTarget, selectedRoleUserIds, newStudentNumber, newMemberName, actionMenuAnchor and related handlers); extract all of that into a new page hook (e.g., useManagedMemberListPage) under hooks/managedMemberList so the hook initializes those mutations and state and exposes the handlers and values, then update ManagedMemberList to only call useManagedMemberListPage and render UI (keep useParams/useNavigate and any purely UI refs in the component if desired), ensuring the hook returns the mutate functions, isPending flags, lists (managedMemberList, preMembersList) and setters (setSelectedMember, setSelectedPreMember, openRoleManage, closeRoleManage, openAction, closeAction, etc.) so tests and other pages import the logic from the hook instead of the component.
201-201: 조건부 클래스에cn()사용동일하게
cn()유틸리티 적용이 권장됩니다.♻️ 제안
- className={`text-sub2 text-left ${tone === 'danger' ? 'text-[`#FF4E4E`]' : 'text-text-600'}`} + className={cn('text-sub2 text-left', tone === 'danger' ? 'text-[`#FF4E4E`]' : 'text-text-600')}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedMemberList/index.tsx` at line 201, Replace the inline template literal className that uses tone (className={`text-sub2 text-left ${tone === 'danger' ? 'text-[`#FF4E4E`]' : 'text-text-600'}`}) with the cn() utility so conditional classes are composed consistently; import or use the existing cn helper, pass the static classes "text-sub2 text-left" and add a conditional mapping for tone === 'danger' to 'text-[`#FF4E4E`]' otherwise 'text-text-600' (refer to the className prop on the element and the tone variable to locate the code).
56-220: 인라인 컴포넌트를 별도 파일로 분리 고려
RoleManageSelector,MemberCard,ActionPopupMenu등은 재사용 가능성이 있고, 파일이 770줄 이상으로 커졌습니다.components/하위로 분리하면 유지보수가 용이해집니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedMemberList/index.tsx` around lines 56 - 220, Split the large page by extracting the inline components RoleManageSelector, MemberAvatar, MemberCard, ActionPopupMenu, and MemberSection into their own modules and export them for reuse; preserve and export their prop interfaces (e.g., MemberCardProps, PopupMenuItem, MenuAnchor, RoleManageOption) and ensure the new modules import any external hooks/constants used (useClickTouchOutside, ROLE_OPTIONS, ACTION_MENU_WIDTH/ACTION_MENU_MARGIN/ACTION_MENU_OFFSET, Portal, icons, etc.). After moving, replace the inline definitions in the page with imports and keep behavior identical (same props, event handlers, aria attributes); if any component needs a ref or contextual values, add forwarding or prop passthrough as needed. Update all references to those symbols (onAction, onChange handlers, items prop for ActionPopupMenu) to match the exported names and run typechecks to fix any missing imports or exported types. Finally, add simple unit/visual smoke tests or Storybook entries for each extracted component to ensure no regressions.
380-381: Set에서 첫 값 추출 시 더 명확한 방식 고려
Set.values().next().value에 타입 캐스팅보다 배열 변환이 의도가 더 명확합니다.♻️ 제안
- const nextPresidentId = selectedRoleUserIds.values().next().value as number | undefined; + const [nextPresidentId] = [...selectedRoleUserIds];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedMemberList/index.tsx` around lines 380 - 381, selectedRoleUserIds.values().next().value is using a type cast to get the first item; replace it with an explicit array conversion for clarity (e.g., Array.from(selectedRoleUserIds)[0] or [...selectedRoleUserIds][0]) when assigning to nextPresidentId so the intent is clear and you can naturally get number | undefined without forced casting; update the assignment of nextPresidentId accordingly and remove the as number | undefined cast.
75-78: 하드코딩된 색상값 토큰화 고려
#69BFDF,#A5B3C1,#C6CFD8등 반복 사용되는 색상이 있습니다. 코딩 가이드라인에 따라theme.css에 토큰으로 정의하면 일관성 유지가 쉬워집니다.As per coding guidelines: "Prioritize color tokens from
src/styles/theme.css"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedMemberList/index.tsx` around lines 75 - 78, The JSX contains hardcoded hex colors in the container and icon classes (e.g., the className on the wrapper and RoleSelectorArrowDownIcon) — replace repeated hex values like `#A5B3C1`, `#C6CFD8`, `#69BFDF` with the corresponding CSS tokens from src/styles/theme.css (use the theme variable names, e.g., --color-<name>), update the className or style usage on the wrapper element and RoleSelectorArrowDownIcon to reference those tokens (via CSS classes or inline var(...) references), and ensure any new token names are added to theme.css so the component uses theme variables instead of hardcoded hex values.src/pages/User/MyPage/components/UserInfoCard.tsx (2)
6-6: 상대 경로 import를@/*alias로 통일해주세요.현재 경로는 프로젝트 컨벤션과 다릅니다.
제안 수정
-import { useMyInfo } from '../../Profile/hooks/useMyInfo'; +import { useMyInfo } from '@/pages/User/Profile/hooks/useMyInfo';As per coding guidelines "
**/*.{ts,tsx}: Use path alias@/*for imports instead of relative paths".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/User/MyPage/components/UserInfoCard.tsx` at line 6, Update the relative import in UserInfoCard.tsx to use the project path alias instead of a relative path: replace the import of useMyInfo (currently from '../../Profile/hooks/useMyInfo') with the alias-based path (e.g., '@/pages/User/Profile/hooks/useMyInfo'), ensuring the symbol useMyInfo is imported from that aliased module and that tsconfig/webpack path mapping supports '@/'.
35-37: 타이포는 임의 px 대신 토큰(text-h*,text-sub*,text-body*,text-cap*)을 사용해주세요.현재
text-[16px],text-[11px],leading-[15px]는 토큰 우선 원칙과 맞지 않습니다.As per coding guidelines "Use typography tokens (
text-h1throughtext-cap2) fromsrc/styles/theme.css".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/User/MyPage/components/UserInfoCard.tsx` around lines 35 - 37, Replace the hard-coded typography classes in UserInfoCard.tsx (the elements referencing currentClub and myInfo) with the project's typography tokens instead of px values: remove text-[16px] and replace with the appropriate heading token (e.g. text-h4/text-h5), replace text-[11px] with a sub/body/caption token (e.g. text-sub2/text-body2/text-cap2), and replace leading-[15px] with the corresponding line-height token provided by the theme; ensure you update both the club name line and the student/university/position line to use tokens from src/styles/theme.css (text-h*, text-sub*, text-body*, text-cap*), preserving font-weight and color classes.src/components/common/BottomModal.tsx (1)
14-25: 클래스 병합은cn()으로 통일해 주세요.이번에 추가한
overlayClassName도twMerge대신cn()으로 합치면 호출부 동작과 컴포넌트 컨벤션이 일관됩니다.♻️ Suggested change
-import { twMerge } from 'tailwind-merge'; +import { cn } from '@/utils/ts/cn'; @@ -function BottomModal({ isOpen, onClose, children, className, overlayClassName }: BottomModalProps) { +function BottomModal({ isOpen, onClose, children, className, overlayClassName }: BottomModalProps) { @@ - className={twMerge('fixed inset-0 z-100 bg-black/60', overlayClassName)} + className={cn('fixed inset-0 z-100 bg-black/60', overlayClassName)} @@ - className={twMerge('fixed inset-x-0 bottom-0 rounded-t-3xl bg-white', className)} + className={cn('fixed inset-x-0 bottom-0 rounded-t-3xl bg-white', className)}As per coding guidelines,
**/*.{ts,tsx}: Usecn()utility fromsrc/utils/ts/cn.tsto merge Tailwind CSS classes.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/common/BottomModal.tsx` around lines 14 - 25, BottomModal uses twMerge to combine Tailwind classes for the overlayClassName which violates the project convention; replace twMerge with the cn() utility so class merging is consistent. Locate the return JSX in BottomModal (the div with className built from 'fixed inset-0 z-100 bg-black/60' and overlayClassName) and change the merge invocation from twMerge(...) to cn(...), ensuring cn is imported if not already; keep the same argument order so overlayClassName still overrides defaults.src/components/layout/index.tsx (1)
18-18:pt-[63px]를 매직 넘버로 두지 않는 편이 안전합니다.
ManagerHeader쪽은 고정 높이가 없는데 여기만 63px로 하드코딩돼 있어서, 헤더 padding이나 타이포가 바뀌면 상단 여백이 바로 어긋납니다. 헤더 높이를 한 곳에서 정의하고 Layout과 같이 재사용하는 쪽이 유지보수에 안전합니다.Also applies to: 31-31
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/layout/index.tsx` at line 18, The Layout currently hardcodes pt-[63px] based on isManagerHeader, which is a magic number; instead centralize the header height used by ManagerHeader and Layout by exporting a single shared value or CSS variable (eg. HEADER_HEIGHT) from the header component or a shared styles module and use that in index.tsx (referencing isManagerHeader and ManagerHeader) for the padding-top; update both occurrences (lines referenced) to consume that shared constant/CSS variable so header height changes are maintained in one place.src/pages/Manager/ManagedRecruitment/index.tsx (1)
8-8: 상대 경로 대신@/*alias를 써 주세요.이 파일은 다른 내부 import를 이미
@/로 통일하고 있어서 여기만 상대 경로로 남아 있으면 이동/정렬 시 관리가 불편합니다.As per coding guidelines,
**/*.{ts,tsx}: Use path alias@/*for imports instead of relative paths.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedRecruitment/index.tsx` at line 8, Replace the relative import of useGetClubSettings with the project path alias; update the import statement that currently references "../hooks/useManagedSettings" to use the alias form (e.g. "@/pages/Manager/ManagedRecruitment/hooks/useManagedSettings") so useGetClubSettings is imported via the `@/` path like the other internal imports in this file.
🤖 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/common/ToggleSwitch.tsx`:
- Around line 77-95: Update the ToggleSwitch component's isManager branch so the
clickable/touch target and keyboard focus are preserved: keep the outer wrapper
size consistent with non-manager variant (add padding/touch-manipulation or an
invisible larger hit-area element), reintroduce focus-visible ring classes on
the wrapper, and ensure keyboard accessibility by adding role="switch",
aria-checked based on enabled, and tabIndex handling on the interactive element;
adjust the inner knob translation logic (the span that uses enabled ?
'translate-x-[17px]' : 'translate-x-0') to match the larger hit area so visual
position remains correct. References: isManager, enabled, isHorizontal, the
wrapper className conditional and the inner knob span.
In `@src/components/layout/Header/components/ManagerHeader.tsx`:
- Around line 11-18: The title (span {title}) in ManagerHeader can grow and push
NotificationBell off-screen; wrap or adjust the title area so it truncates: make
the left container a flex item that allows shrinking (add min-w-0 to the parent
div with class "flex items-center gap-1" or make it flex-1) and ensure the title
span uses single-line truncation (overflow-hidden, whitespace-nowrap,
text-overflow: ellipsis / "truncate"). This ensures long clubName values
truncate instead of overlapping NotificationBell and preserves layout for
ManagerHeader and its children (title, smartBack, NotificationBell).
In `@src/components/layout/Header/routeTitles.ts`:
- Around line 23-42: Add a new route entry in
src/components/layout/Header/routeTitles.ts that matches the applicants list
path (e.g. a match function using
/^\/mypage\/manager\/\d+\/applications$/.test(pathname)) and set its title to
"지원자 목록"; insert this entry into the existing array of { match, title } objects
(near the other /mypage/manager entries) so the header renders the correct title
for /mypage/manager/:clubId/applications.
In `@src/pages/Manager/ManagedAccount/index.tsx`:
- Line 28: The patch mutation errors from usePatchClubSettings are not surfaced
to users: update the hook destructure to also extract the error state (e.g.,
error: patchError and isError) from usePatchClubSettings alongside mutate:
patchSettings and isPending: isPatchPending, then surface patchError wherever
the component currently renders API errors (merge it into the same error display
state or pass it into the existing error component). Also add simple
error-handling paths in the places that call patchSettings (ToggleSwitch handler
and enableAfterSave flow) to ensure the UI shows the mutation error (use the
hook's error or reject handler to set the same error state). Ensure you
reference usePatchClubSettings, patchSettings, isPatchPending, ToggleSwitch, and
enableAfterSave when making these changes.
In `@src/pages/Manager/ManagedApplicationList/index.tsx`:
- Around line 40-43: 현재 클릭 가능한 div (the div wrapping the item that calls
onDetail(application.id)) only has onClick and lacks keyboard accessibility;
update that element to be focusable and operable by keyboard by adding
role="button", tabIndex={0}, and an onKeyDown handler that triggers
onDetail(application.id) when Enter or Space is pressed (for Space
preventDefault to avoid page scroll), and ensure any existing onClick behavior
is preserved; target the div that contains onClick={() =>
onDetail(application.id)} and implement these handlers there.
- Around line 130-131: In the ManagedApplicationList component update the span
rendering the header (the JSX span with className that currently includes
"text-text-600") to use a valid theme color token (e.g. replace "text-text-600"
with a defined class such as "text-indigo-600" or the project's equivalent);
locate the span in ManagedApplicationList (the element showing "대기중
{managedClubApplicationList?.totalCount ?? 0}명") and swap the invalid token for
the correct color class so it matches theme.css.
- Around line 153-155: Update the CSS utility token in the render where the
observer div is defined inside the ManagedApplicationList component: replace the
className token "text-caption1" with the guideline-preferred "text-cap1" so the
div using observerRef and isFetchingNextPage uses "text-cap1 flex h-12
items-center justify-center text-indigo-300" instead of "text-caption1 ...".
In `@src/pages/Manager/ManagedClubList/index.tsx`:
- Around line 21-29: The text content can overflow and push the chevron; ensure
the span container and spans can shrink and truncate by adding min-w-0 to the
inner div ("div className='flex items-center gap-1.5'") and adding the Tailwind
"truncate" class to the name and category spans (the elements with className
"text-sub2 text-indigo-700" and "text-cap1 text-indigo-300") so long names are
clipped with an ellipsis instead of expanding the card.
In `@src/pages/Manager/ManagedClubProfile/index.tsx`:
- Around line 185-225: The labels are not associated with their inputs; update
the readOnlyFields map rendering and the individual inputs for description,
location, and introduce so each input/textarea has a unique id and each
corresponding label uses htmlFor to reference that id (e.g., generate ids using
the label or index). Specifically modify the block that maps readOnlyFields (the
JSX using readOnlyFields.map), the description input (value={description},
onChange={handleDescriptionChange}), the location input (onChange={(e) =>
setLocation(e.target.value)}), and the introduce textarea (onChange={(e) =>
setIntroduce(e.target.value)}) to include matching id attributes while
preserving DESCRIPTION_MAX_LENGTH, fieldLabelClassName, fieldInputClassName, and
fieldTextAreaClassName.
In `@src/pages/Manager/ManagedMemberList/index.tsx`:
- Line 422: Replace the hardcoded query key array used in ManagedMemberList's
cache invalidation with the exported factory from memberQueryKeys: instead of
calling queryClient.invalidateQueries({ queryKey: ['manager', 'managedMembers',
clubId] }), import memberQueryKeys and use its factory function (or array
builder) to produce the key for invalidateQueries; update the import at the top
of ManagedMemberList/index.tsx to import memberQueryKeys and pass
memberQueryKeys.managedMembers(clubId) (or the equivalent factory method) into
queryClient.invalidateQueries so key structure stays centralized and in sync.
In `@src/pages/Manager/ManagedRecruitment/index.tsx`:
- Around line 44-45: The row icon image inside the Link (the <Link ...>
containing <img src={icon} ... /> in ManagedRecruitment) is decorative and
should not duplicate the link text; change the image's alt from {title} to an
empty string and add aria-hidden="true" on the <img> element so screen readers
ignore it while keeping the visible title for users.
In `@src/pages/Manager/ManagedRecruitmentForm/index.tsx`:
- Around line 62-64: The application-enable toggle handler
(handleApplicationEnabledChange) currently calls patchSettings immediately and
can enable applications even when there are empty questions or a pending save;
modify handleApplicationEnabledChange to first check hasEmptyQuestion and
isPending and refuse to call patchSettings if either is true (or defer the
change until after a successful submit), and also ensure the UI toggle is
disabled when hasEmptyQuestion || isPending so users cannot interact with it;
update any other similar handlers around lines shown (e.g., the other toggle
handlers at 100-106 and 184-185) to use the same guard or deferred-apply pattern
and only call patchSettings after a successful save response.
In `@src/pages/Manager/ManagedRecruitmentWrite/index.tsx`:
- Around line 191-193: The top toggle handler (handleRecruitmentEnabledChange)
currently calls patchSettings immediately which can activate recruitment even if
required fields are empty or save fails; instead keep the toggle value in local
component state (e.g., update a local isRecruitmentEnabled state/flag) and do
not call patchSettings from handleRecruitmentEnabledChange; update handleSubmit
to read that local flag and, on successful save, call patchSettings or reuse the
existing enableAfterSave branch to persist the change. Apply the same change to
the other toggle handler referenced around the 244-250 area so no toggle
directly calls patchSettings outside the submit/success flow.
- Around line 171-176: The component revokes preview URLs only when a single
image is deleted (targetImage) but never on unmount, leaking blob URLs; to fix,
add an imagesRef (useRef<ImageItem[]>) that is updated in a useEffect([images])
to mirror the images state, and add a useEffect with an empty deps array that
returns a cleanup function which iterates imagesRef.current and calls
URL.revokeObjectURL(image.previewUrl) for each image where image.isExisting is
false; keep the existing per-delete revoke but ensure the global unmount cleanup
covers navigation/save/unmount paths to avoid leaks.
In `@src/pages/User/MyPage/components/UserInfoCard.tsx`:
- Line 2: The imported SVG name and the variable name disagree: the code imports
"Chevron-left-dark.svg" into the variable RightArrowIcon; update the import so
the asset and variable describe the same direction (either import
"Chevron-right-..." into RightArrowIcon or rename the variable to
LeftArrowIcon), e.g., change the import target to the right-facing SVG or rename
RightArrowIcon to match Chevron-left-dark.svg; ensure the symbol RightArrowIcon
(or the renamed variable) is used consistently in UserInfoCard to reflect the
correct arrow direction.
- Line 100: The clickable div using cardClassName in UserInfoCard.tsx lacks
keyboard accessibility; update the element so when isClickable is true it
includes role="button", tabIndex={0}, and an onKeyDown handler that invokes
handleCardClick when Enter or Space is pressed (and does nothing otherwise),
while ensuring these attributes/handlers are omitted or disabled when
isClickable is false to preserve semantics.
---
Outside diff comments:
In `@src/pages/Manager/ManagedClubProfile/index.tsx`:
- Around line 107-126: The submit flow currently uses isUploading for the whole
submission so the main button shows "이미지 업로드 중..." even when no upload occurs;
update handleSubmit to only toggle isUploading around the uploadImage call
(setIsUploading(true) immediately before uploadImage and setIsUploading(false)
right after its result) and introduce a separate submitting flag (e.g.,
isSubmitting with setIsSubmitting) to represent the overall form submission
around updateClubInfo; adjust the UI text checks to use isUploading for
image-only status and isSubmitting for overall submit state, and apply the same
change to the other handler referenced (the code at the second occurrence).
---
Nitpick comments:
In `@src/components/common/BottomModal.tsx`:
- Around line 14-25: BottomModal uses twMerge to combine Tailwind classes for
the overlayClassName which violates the project convention; replace twMerge with
the cn() utility so class merging is consistent. Locate the return JSX in
BottomModal (the div with className built from 'fixed inset-0 z-100 bg-black/60'
and overlayClassName) and change the merge invocation from twMerge(...) to
cn(...), ensuring cn is imported if not already; keep the same argument order so
overlayClassName still overrides defaults.
In `@src/components/layout/index.tsx`:
- Line 18: The Layout currently hardcodes pt-[63px] based on isManagerHeader,
which is a magic number; instead centralize the header height used by
ManagerHeader and Layout by exporting a single shared value or CSS variable (eg.
HEADER_HEIGHT) from the header component or a shared styles module and use that
in index.tsx (referencing isManagerHeader and ManagerHeader) for the
padding-top; update both occurrences (lines referenced) to consume that shared
constant/CSS variable so header height changes are maintained in one place.
In `@src/pages/Manager/components/ApplicationDetailContent.tsx`:
- Around line 113-124: The image modal only closes via backdrop click; add
keyboard handling so pressing Escape also closes it: when rendering the
Portal-backed backdrop div (the element using isImageOpen and
application.feePaymentImageUrl), attach an onKeyDown handler that calls
closeImage when event.key === 'Escape', make that div focusable (e.g.,
tabIndex={-1}) and move focus to it when isImageOpen becomes true (or register a
document keydown listener in a useEffect that cleans up) to ensure the Escape
key is caught; keep existing stopPropagation on the <img> and ensure the key
handler is removed when the modal closes.
- Around line 17-18: Replace hardcoded font-size utilities (e.g. text-[20px],
text-[15px], text-[13px]) in ApplicationDetailContent.tsx with the project's
semantic typography tokens (e.g. text-body1, text-body2, text-sub1). Locate the
JSX elements that render the avatar initial (the div using name.charAt(0)) and
the other text spans in this component (the other occurrences referenced in the
review) and swap each custom size class for the appropriate semantic class to
preserve visual hierarchy (20px→text-body1, 15px→text-body2, 13px→text-sub1 or
equivalent token per your design system). Ensure you update every listed
instance in the file so styling is consistent and no custom text-[...] classes
remain.
- Around line 15-29: This component (ApplicationDetailContent.tsx) uses
hard-coded hex colors in className strings (e.g., border-[`#F4F6F9`],
bg-[`#E7EBEF`], text-[`#5A6B7F`]); replace those hex values with the corresponding
design tokens from src/styles/theme.css (use the appropriate indigo-*, blue-*,
or text-* tokens) inside the same className attributes (for example swap
border-[`#F4F6F9`] to the matching border token, bg-[`#E7EBEF`] to the matching bg
token, and text-[`#5A6B7F`] to the matching text token) so the visual styles rely
on the centralized theme rather than hard-coded hex values.
In `@src/pages/Manager/hooks/useManagedApplications.ts`:
- Around line 120-127: The mutation returned by useManagedApplications (the
useMutation call using postClubApplicationApprove and queryKey
applicationQueryKeys.managedClubApplications) lacks an onError handler; add an
onError callback to the useMutation that calls showToast with a meaningful error
message (and any returned API error details) and optionally logs the error, and
ensure you mirror the same onError addition for the other mutation block (the
one at lines 136-143) so both approve and reject flows report failures to the
user; keep existing onSuccess behavior (invalidateQueries and navigate back)
unchanged.
- Around line 71-72: Rename the misleading variable managedClubApplicationList
to a clearer name that reflects it holds the first page response (e.g.,
firstPageResponse or firstPageData) and update all references where it's used;
specifically change the declaration const managedClubApplicationList =
data.pages[0] ?? null to use the new identifier and ensure any code that relied
on managedClubApplicationList (such as accessing currentPage, totalPage, etc.)
is updated to the new name, while keeping applications =
data.pages.flatMap((page) => page?.applications ?? []) unchanged.
In `@src/pages/Manager/ManagedApplicationDetail/index.tsx`:
- Line 10: 해당 파일의 import 구문에서 상대경로 '../hooks/useManagedApplications' 대신 프로젝트 경로
별칭을 사용하도록 변경하세요; 구체적으로 ManagedApplicationDetail 컴포넌트에서 사용된
useManagedApplications import를 '@/pages/Manager/hooks/useManagedApplications' 같은
별칭 경로로 교체하도록 업데이트하면 됩니다.
- Around line 74-90: The three repeated button elements (the Cancel button using
closeApprove and the Approve button using handleApprove, observing isPending and
isApproving) duplicate almost identical className strings; extract a shared
Button component or a constant for the shared className and use props to vary
onClick, disabled, background color or label (e.g., create ManagedActionButton
or BUTTON_BASE_CLASS and reuse it in the Cancel/Approve/Reject locations,
passing isPending/isApproving to control disabled/label and preserving existing
handlers closeApprove and handleApprove).
- Around line 49-66: Replace hardcoded "#69BFDF" color usages in the two buttons
inside the ManagedApplicationDetail component with the design tokens from
theme.css (e.g., use classes like "bg-primary", "border-primary", "text-primary"
or "blue-400" equivalents) and/or refactor these buttons to use the shared
Button component if one exists; update the JSX where openReject/openApprove are
used (and the className strings referencing the hex) to apply the token classes
and ensure disabled styles still use disabled:opacity-50 and
disabled:cursor-not-allowed while preserving conditional labels driven by
isRejecting/isApproving and the disabled prop bound to isPending.
In `@src/pages/Manager/ManagedApplicationList/index.tsx`:
- Around line 47-52: Replace hard-coded pixel typography classes with the
project's semantic typography tokens: change the className containing
"text-[15px] leading-6 font-semibold text-indigo-700" to use the semantic token
(e.g., "text-body1") while preserving other utilities like leading, weight, and
color, and change the className containing "text-[13px] leading-[1.6]
font-medium text-indigo-300" to use the semantic token (e.g., "text-sub1");
update in the ManagedApplicationList JSX where these className strings are
defined so the visual size maps to the guideline tokens (text-body1, text-sub1)
instead of raw pixel values.
In `@src/pages/Manager/ManagedMemberApplicationDetail/index.tsx`:
- Line 3: Update the import in ManagedMemberApplicationDetail's module to use
the project path alias instead of a relative path: replace the relative import
of useGetManagedMemberApplicationDetailByUser from
'../hooks/useManagedApplications' with the aliased path
'@/pages/Manager/hooks/useManagedApplications' (keep the imported symbol name
useGetManagedMemberApplicationDetailByUser unchanged) so the file index.tsx uses
the recommended `@/`* alias.
In `@src/pages/Manager/ManagedMemberList/index.tsx`:
- Around line 414-420: The loops over promoteUserIds and demoteUserIds currently
await each changeMemberPosition call sequentially, causing slower overall
execution; change them to run in parallel by mapping each id to
changeMemberPosition and awaiting Promise.all for each group (i.e., await
Promise.all(promoteUserIds.map(id => changeMemberPosition(...))) and similarly
for demoteUserIds) so requests are issued concurrently while preserving the
overall behavior; reference the promoteUserIds, demoteUserIds arrays and the
changeMemberPosition function when making the change.
- Around line 91-93: Replace the conditional template literal used in the
className prop inside the ManagedMemberList JSX with the cn() utility to merge
Tailwind classes; specifically update the className that references index and
ROLE_OPTIONS so it calls cn('text-sub2 text-text-600 w-full px-3 py-[3px]
text-left', { 'border-b border-[`#C6CFD8`]': index !== ROLE_OPTIONS.length - 1 })
(or equivalent object/array form) to follow the project's Tailwind class merging
guideline and ensure consistent class composition.
- Around line 222-268: The page component ManagedMemberList currently contains
business logic and state (calls to useManagedMembers, useTransferPresident,
useChangeVicePresident, useChangeMemberPosition, useRemoveMember,
useAddPreMember, useGetPreMemberList, useDeletePreMember plus state like
selectedMember, selectedPreMember, isRoleManageOpen, isActionOpen,
roleManageTarget, selectedRoleUserIds, newStudentNumber, newMemberName,
actionMenuAnchor and related handlers); extract all of that into a new page hook
(e.g., useManagedMemberListPage) under hooks/managedMemberList so the hook
initializes those mutations and state and exposes the handlers and values, then
update ManagedMemberList to only call useManagedMemberListPage and render UI
(keep useParams/useNavigate and any purely UI refs in the component if desired),
ensuring the hook returns the mutate functions, isPending flags, lists
(managedMemberList, preMembersList) and setters (setSelectedMember,
setSelectedPreMember, openRoleManage, closeRoleManage, openAction, closeAction,
etc.) so tests and other pages import the logic from the hook instead of the
component.
- Line 201: Replace the inline template literal className that uses tone
(className={`text-sub2 text-left ${tone === 'danger' ? 'text-[`#FF4E4E`]' :
'text-text-600'}`}) with the cn() utility so conditional classes are composed
consistently; import or use the existing cn helper, pass the static classes
"text-sub2 text-left" and add a conditional mapping for tone === 'danger' to
'text-[`#FF4E4E`]' otherwise 'text-text-600' (refer to the className prop on the
element and the tone variable to locate the code).
- Around line 56-220: Split the large page by extracting the inline components
RoleManageSelector, MemberAvatar, MemberCard, ActionPopupMenu, and MemberSection
into their own modules and export them for reuse; preserve and export their prop
interfaces (e.g., MemberCardProps, PopupMenuItem, MenuAnchor, RoleManageOption)
and ensure the new modules import any external hooks/constants used
(useClickTouchOutside, ROLE_OPTIONS,
ACTION_MENU_WIDTH/ACTION_MENU_MARGIN/ACTION_MENU_OFFSET, Portal, icons, etc.).
After moving, replace the inline definitions in the page with imports and keep
behavior identical (same props, event handlers, aria attributes); if any
component needs a ref or contextual values, add forwarding or prop passthrough
as needed. Update all references to those symbols (onAction, onChange handlers,
items prop for ActionPopupMenu) to match the exported names and run typechecks
to fix any missing imports or exported types. Finally, add simple unit/visual
smoke tests or Storybook entries for each extracted component to ensure no
regressions.
- Around line 380-381: selectedRoleUserIds.values().next().value is using a type
cast to get the first item; replace it with an explicit array conversion for
clarity (e.g., Array.from(selectedRoleUserIds)[0] or
[...selectedRoleUserIds][0]) when assigning to nextPresidentId so the intent is
clear and you can naturally get number | undefined without forced casting;
update the assignment of nextPresidentId accordingly and remove the as number |
undefined cast.
- Around line 75-78: The JSX contains hardcoded hex colors in the container and
icon classes (e.g., the className on the wrapper and RoleSelectorArrowDownIcon)
— replace repeated hex values like `#A5B3C1`, `#C6CFD8`, `#69BFDF` with the
corresponding CSS tokens from src/styles/theme.css (use the theme variable
names, e.g., --color-<name>), update the className or style usage on the wrapper
element and RoleSelectorArrowDownIcon to reference those tokens (via CSS classes
or inline var(...) references), and ensure any new token names are added to
theme.css so the component uses theme variables instead of hardcoded hex values.
In `@src/pages/Manager/ManagedRecruitment/index.tsx`:
- Line 8: Replace the relative import of useGetClubSettings with the project
path alias; update the import statement that currently references
"../hooks/useManagedSettings" to use the alias form (e.g.
"@/pages/Manager/ManagedRecruitment/hooks/useManagedSettings") so
useGetClubSettings is imported via the `@/` path like the other internal imports
in this file.
In `@src/pages/User/MyPage/components/UserInfoCard.tsx`:
- Line 6: Update the relative import in UserInfoCard.tsx to use the project path
alias instead of a relative path: replace the import of useMyInfo (currently
from '../../Profile/hooks/useMyInfo') with the alias-based path (e.g.,
'@/pages/User/Profile/hooks/useMyInfo'), ensuring the symbol useMyInfo is
imported from that aliased module and that tsconfig/webpack path mapping
supports '@/'.
- Around line 35-37: Replace the hard-coded typography classes in
UserInfoCard.tsx (the elements referencing currentClub and myInfo) with the
project's typography tokens instead of px values: remove text-[16px] and replace
with the appropriate heading token (e.g. text-h4/text-h5), replace text-[11px]
with a sub/body/caption token (e.g. text-sub2/text-body2/text-cap2), and replace
leading-[15px] with the corresponding line-height token provided by the theme;
ensure you update both the club name line and the student/university/position
line to use tokens from src/styles/theme.css (text-h*, text-sub*, text-body*,
text-cap*), preserving font-weight and color classes.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: fc15e660-370e-4f04-819a-b44443262f8c
⛔ Files ignored due to path filters (11)
src/assets/image/3d-card.pngis excluded by!**/*.png,!src/assets/**and included by**src/assets/image/3d-file.pngis excluded by!**/*.png,!src/assets/**and included by**src/assets/image/3d-flag.pngis excluded by!**/*.png,!src/assets/**and included by**src/assets/image/boy.pngis excluded by!**/*.png,!src/assets/**and included by**src/assets/image/chat.pngis excluded by!**/*.png,!src/assets/**and included by**src/assets/image/folder.pngis excluded by!**/*.png,!src/assets/**and included by**src/assets/svg/Chevron-left-dark.svgis excluded by!**/*.svg,!src/assets/**and included by**src/assets/svg/add-photo-alternate.svgis excluded by!**/*.svg,!src/assets/**and included by**src/assets/svg/close.svgis excluded by!**/*.svg,!src/assets/**and included by**src/assets/svg/more-horizontal.svgis excluded by!**/*.svg,!src/assets/**and included by**src/assets/svg/role-selector-arrow-down.svgis excluded by!**/*.svg,!src/assets/**and included by**
📒 Files selected for processing (23)
src/components/common/BottomModal.tsxsrc/components/common/ToggleSwitch.tsxsrc/components/layout/Header/components/ManagerHeader.tsxsrc/components/layout/Header/headerConfig.tssrc/components/layout/Header/index.tsxsrc/components/layout/Header/routeTitles.tssrc/components/layout/Header/types.tssrc/components/layout/index.tsxsrc/pages/Manager/ManagedAccount/index.tsxsrc/pages/Manager/ManagedApplicationDetail/index.tsxsrc/pages/Manager/ManagedApplicationList/index.tsxsrc/pages/Manager/ManagedClubDetail/index.tsxsrc/pages/Manager/ManagedClubList/index.tsxsrc/pages/Manager/ManagedClubProfile/index.tsxsrc/pages/Manager/ManagedMemberApplicationDetail/index.tsxsrc/pages/Manager/ManagedMemberList/index.tsxsrc/pages/Manager/ManagedRecruitment/index.tsxsrc/pages/Manager/ManagedRecruitmentForm/index.tsxsrc/pages/Manager/ManagedRecruitmentWrite/index.tsxsrc/pages/Manager/components/ApplicationDetailContent.tsxsrc/pages/Manager/hooks/useManagedApplications.tssrc/pages/Manager/hooks/useManagedMembers.tssrc/pages/User/MyPage/components/UserInfoCard.tsx
| isManager | ||
| ? 'relative h-5 w-[37px] rounded-full transition-colors focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-60' | ||
| : 'relative touch-manipulation rounded-full transition-colors focus-visible:ring-2 focus-visible:ring-indigo-300 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-60', | ||
| isManager | ||
| ? enabled | ||
| ? 'bg-primary-500' | ||
| : 'bg-text-100' | ||
| : isHorizontal | ||
| ? `h-7 w-12 border border-indigo-50 ${enabled ? 'bg-indigo-700' : 'bg-indigo-50'}` | ||
| : `h-5 w-9 ${enabled ? 'bg-primary' : 'bg-indigo-100'}` | ||
| )} | ||
| > | ||
| {isHorizontal ? ( | ||
| {isManager ? ( | ||
| <span | ||
| className={twMerge( | ||
| 'absolute top-0.5 left-0.5 size-4 rounded-full bg-white shadow-[0_0_3px_rgba(0,0,0,0.15)] transition-transform', | ||
| enabled ? 'translate-x-[17px]' : 'translate-x-0' | ||
| )} | ||
| /> |
There was a problem hiding this comment.
manager 변형이 터치/키보드 접근성을 너무 줄입니다.
이 분기에서는 실제 클릭 영역이 20x37 정도로 작아지고 focus-visible 표시도 사라집니다. 모바일 WebView에서는 누르기 어렵고, 키보드 포커스도 보이지 않아서 시각적인 트랙은 유지하더라도 버튼 hit area와 포커스 링은 따로 확보하는 편이 좋습니다.
As per coding guidelines, src/components/**: React 컴포넌트 컨벤션을 확인해주세요: - 접근성(aria-*, role, 키보드 탐색)이 적절히 처리되는지.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/common/ToggleSwitch.tsx` around lines 77 - 95, Update the
ToggleSwitch component's isManager branch so the clickable/touch target and
keyboard focus are preserved: keep the outer wrapper size consistent with
non-manager variant (add padding/touch-manipulation or an invisible larger
hit-area element), reintroduce focus-visible ring classes on the wrapper, and
ensure keyboard accessibility by adding role="switch", aria-checked based on
enabled, and tabIndex handling on the interactive element; adjust the inner knob
translation logic (the span that uses enabled ? 'translate-x-[17px]' :
'translate-x-0') to match the larger hit area so visual position remains
correct. References: isManager, enabled, isHorizontal, the wrapper className
conditional and the inner knob span.
| { | ||
| match: (pathname) => /^\/mypage\/manager\/\d+\/applications\/\d+$/.test(pathname), | ||
| title: '지원서 보기', | ||
| }, | ||
| { | ||
| match: (pathname) => /^\/mypage\/manager\/\d+\/recruitment$/.test(pathname), | ||
| title: '모집 공고 및 지원서 관리', | ||
| }, | ||
| { | ||
| match: (pathname) => /^\/mypage\/manager\/\d+\/recruitment\/write$/.test(pathname), | ||
| title: '모집 공고', | ||
| }, | ||
| { | ||
| match: (pathname) => /^\/mypage\/manager\/\d+\/recruitment\/form$/.test(pathname), | ||
| title: '지원서', | ||
| }, | ||
| { | ||
| match: (pathname) => /^\/mypage\/manager\/\d+\/recruitment\/account$/.test(pathname), | ||
| title: '가입비', | ||
| }, |
There was a problem hiding this comment.
지원자 목록 페이지 타이틀이 빠져 있습니다.
src/App.tsx에는 /mypage/manager/:clubId/applications 라우트가 있는데 여기엔 매칭이 없습니다. 지금 headerConfig.ts가 manager 하위 경로 전체를 manager 헤더로 잡고 있어서, 이 페이지는 빈 제목으로 렌더링됩니다.
수정 예시
{
+ match: (pathname) => /^\/mypage\/manager\/\d+\/applications$/.test(pathname),
+ title: '지원자 관리',
+ },
+ {
match: (pathname) => /^\/mypage\/manager\/\d+\/applications\/\d+$/.test(pathname),
title: '지원서 보기',
},🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/layout/Header/routeTitles.ts` around lines 23 - 42, Add a new
route entry in src/components/layout/Header/routeTitles.ts that matches the
applicants list path (e.g. a match function using
/^\/mypage\/manager\/\d+\/applications$/.test(pathname)) and set its title to
"지원자 목록"; insert this entry into the existing array of { match, title } objects
(near the other /mypage/manager entries) so the header renders the correct title
for /mypage/manager/:clubId/applications.
| const { data: clubSettings } = useGetClubSettings(clubIdNumber); | ||
| const { mutate, isPending, error } = useManagedClubFeeMutation(clubIdNumber); | ||
| const { mutate: patchSettings } = usePatchClubSettings(clubIdNumber); | ||
| const { mutate: patchSettings, isPending: isPatchPending } = usePatchClubSettings(clubIdNumber); |
There was a problem hiding this comment.
회비 설정 PATCH 실패가 사용자에게 보이지 않습니다.
ToggleSwitch 경로와 enableAfterSave 경로 둘 다 patchSettings를 호출하지만, 현재는 usePatchClubSettings의 에러 상태를 읽지 않아 실패가 무반응처럼 보입니다. 최소한 patch mutation error도 기존 에러 영역에 같이 노출하는 편이 안전합니다.
수정 예시
- const { mutate: patchSettings, isPending: isPatchPending } = usePatchClubSettings(clubIdNumber);
+ const { mutate: patchSettings, isPending: isPatchPending, error: patchError } =
+ usePatchClubSettings(clubIdNumber);
...
const errorMessage =
(isApiError(error) ? error.apiError?.fieldErrors?.[0]?.message : undefined) ??
+ (isApiError(patchError) ? patchError.apiError?.fieldErrors?.[0]?.message : undefined) ??
error?.message ??
+ patchError?.message ??
'회비 정보 저장에 실패했습니다.';
...
- {error && <p className="text-body3 text-danger-700">{errorMessage}</p>}
+ {(error || patchError) && <p className="text-body3 text-danger-700">{errorMessage}</p>}Also applies to: 65-81, 158-159
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/Manager/ManagedAccount/index.tsx` at line 28, The patch mutation
errors from usePatchClubSettings are not surfaced to users: update the hook
destructure to also extract the error state (e.g., error: patchError and
isError) from usePatchClubSettings alongside mutate: patchSettings and
isPending: isPatchPending, then surface patchError wherever the component
currently renders API errors (merge it into the same error display state or pass
it into the existing error component). Also add simple error-handling paths in
the places that call patchSettings (ToggleSwitch handler and enableAfterSave
flow) to ensure the UI shows the mutation error (use the hook's error or reject
handler to set the same error state). Ensure you reference usePatchClubSettings,
patchSettings, isPatchPending, ToggleSwitch, and enableAfterSave when making
these changes.
| <div | ||
| className="border-indigo-5 active:bg-indigo-5/60 flex cursor-pointer items-center justify-between rounded-2xl border bg-white p-3" | ||
| onClick={() => onDetail(application.id)} | ||
| > |
There was a problem hiding this comment.
클릭 가능한 div에 키보드 접근성 누락
div에 onClick만 있고 role, tabIndex, onKeyDown 핸들러가 없어 키보드 사용자가 접근할 수 없습니다.
♿ 접근성 개선 제안
<div
- className="border-indigo-5 active:bg-indigo-5/60 flex cursor-pointer items-center justify-between rounded-2xl border bg-white p-3"
+ role="button"
+ tabIndex={0}
+ className="border-indigo-5 active:bg-indigo-5/60 flex cursor-pointer items-center justify-between rounded-2xl border bg-white p-3"
onClick={() => onDetail(application.id)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ onDetail(application.id);
+ }
+ }}
>📝 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.
| <div | |
| className="border-indigo-5 active:bg-indigo-5/60 flex cursor-pointer items-center justify-between rounded-2xl border bg-white p-3" | |
| onClick={() => onDetail(application.id)} | |
| > | |
| <div | |
| role="button" | |
| tabIndex={0} | |
| className="border-indigo-5 active:bg-indigo-5/60 flex cursor-pointer items-center justify-between rounded-2xl border bg-white p-3" | |
| onClick={() => onDetail(application.id)} | |
| onKeyDown={(e) => { | |
| if (e.key === 'Enter' || e.key === ' ') { | |
| e.preventDefault(); | |
| onDetail(application.id); | |
| } | |
| }} | |
| > |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/Manager/ManagedApplicationList/index.tsx` around lines 40 - 43, 현재
클릭 가능한 div (the div wrapping the item that calls onDetail(application.id)) only
has onClick and lacks keyboard accessibility; update that element to be
focusable and operable by keyboard by adding role="button", tabIndex={0}, and an
onKeyDown handler that triggers onDetail(application.id) when Enter or Space is
pressed (for Space preventDefault to avoid page scroll), and ensure any existing
onClick behavior is preserved; target the div that contains onClick={() =>
onDetail(application.id)} and implement these handlers there.
| const targetImage = images[currentImageIndex]; | ||
|
|
||
| if (!targetImage) return; | ||
| if (!targetImage.isExisting) { | ||
| URL.revokeObjectURL(targetImage.previewUrl); | ||
| } |
There was a problem hiding this comment.
blob URL 정리가 unmount 경로에는 없습니다.
여기서 삭제 시 revokeObjectURL을 추가한 건 좋지만, 저장 후 이동하거나 뒤로 나가는 경로에서는 생성한 preview URL을 정리하지 않습니다. 모바일 WebView에서는 이 blob이 남아서 메모리를 오래 잡을 수 있습니다.
♻️ Suggested cleanup
const imagesRef = useRef<ImageItem[]>([]);
useEffect(() => {
imagesRef.current = images;
}, [images]);
useEffect(() => {
return () => {
imagesRef.current.forEach((image) => {
if (!image.isExisting) {
URL.revokeObjectURL(image.previewUrl);
}
});
};
}, []);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/Manager/ManagedRecruitmentWrite/index.tsx` around lines 171 - 176,
The component revokes preview URLs only when a single image is deleted
(targetImage) but never on unmount, leaking blob URLs; to fix, add an imagesRef
(useRef<ImageItem[]>) that is updated in a useEffect([images]) to mirror the
images state, and add a useEffect with an empty deps array that returns a
cleanup function which iterates imagesRef.current and calls
URL.revokeObjectURL(image.previewUrl) for each image where image.isExisting is
false; keep the existing per-delete revoke but ensure the global unmount cleanup
covers navigation/save/unmount paths to avoid leaks.
| @@ -1,5 +1,5 @@ | |||
| import { useNavigate, useParams } from 'react-router-dom'; | |||
| import RightArrowIcon from '@/assets/svg/chevron-right.svg'; | |||
| import RightArrowIcon from '@/assets/svg/Chevron-left-dark.svg'; | |||
There was a problem hiding this comment.
아이콘 변수명과 실제 SVG 방향이 불일치해 보입니다.
RightArrowIcon인데 Chevron-left-dark.svg를 가져오고 있어, 화살표 방향이 의도와 다르게 보일 가능성이 큽니다. 에셋 방향 또는 변수명을 일치시켜 주세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/User/MyPage/components/UserInfoCard.tsx` at line 2, The imported
SVG name and the variable name disagree: the code imports
"Chevron-left-dark.svg" into the variable RightArrowIcon; update the import so
the asset and variable describe the same direction (either import
"Chevron-right-..." into RightArrowIcon or rename the variable to
LeftArrowIcon), e.g., change the import target to the right-facing SVG or rename
RightArrowIcon to match Chevron-left-dark.svg; ensure the symbol RightArrowIcon
(or the renamed variable) is used consistently in UserInfoCard to reflect the
correct arrow direction.
|
|
||
| return ( | ||
| <Card {...cardProps}> | ||
| <div className={cardClassName} onClick={isClickable ? handleCardClick : undefined}> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd "UserInfoCard.tsx" --type fRepository: BCSDLab/KONECT_FRONT_END
Length of output: 117
🏁 Script executed:
cat -n "src/pages/User/MyPage/components/UserInfoCard.tsx" | head -150Repository: BCSDLab/KONECT_FRONT_END
Length of output: 6519
클릭 가능한 div에 키보드 접근성이 없습니다.
현재 마우스 클릭만 작동하고, 키보드 사용자는 접근 불가능합니다. role, tabIndex, onKeyDown(Enter/Space)를 추가하세요.
제안 수정
+ const handleCardKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
+ if (!isClickable) return;
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ handleCardClick();
+ }
+ };
- <div className={cardClassName} onClick={isClickable ? handleCardClick : undefined}>
+ <div
+ className={cardClassName}
+ onClick={isClickable ? handleCardClick : undefined}
+ role={isClickable ? 'button' : undefined}
+ tabIndex={isClickable ? 0 : undefined}
+ onKeyDown={isClickable ? handleCardKeyDown : 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.
| <div className={cardClassName} onClick={isClickable ? handleCardClick : undefined}> | |
| const handleCardKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => { | |
| if (!isClickable) return; | |
| if (e.key === 'Enter' || e.key === ' ') { | |
| e.preventDefault(); | |
| handleCardClick(); | |
| } | |
| }; | |
| <div | |
| className={cardClassName} | |
| onClick={isClickable ? handleCardClick : undefined} | |
| role={isClickable ? 'button' : undefined} | |
| tabIndex={isClickable ? 0 : undefined} | |
| onKeyDown={isClickable ? handleCardKeyDown : undefined} | |
| > |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/User/MyPage/components/UserInfoCard.tsx` at line 100, The clickable
div using cardClassName in UserInfoCard.tsx lacks keyboard accessibility; update
the element so when isClickable is true it includes role="button", tabIndex={0},
and an onKeyDown handler that invokes handleCardClick when Enter or Space is
pressed (and does nothing otherwise), while ensuring these attributes/handlers
are omitted or disabled when isClickable is false to preserve semantics.
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/pages/Manager/ManagedRecruitmentForm/index.tsx (1)
84-91:⚠️ Potential issue | 🟡 Minor저장 성공 후 네비게이션 또는 피드백이 누락되었습니다.
enableAfterSave가 없는 일반 저장 시onSuccess콜백에서 아무 동작도 하지 않습니다. 사용자에게 성공 피드백(토스트, 네비게이션 등)이 필요합니다.♻️ 수정 제안
updateQuestions(requestData, { onSuccess: () => { if (location.state?.enableAfterSave) { patchSettings({ isApplicationEnabled: true }, { onSuccess: () => navigate(-1) }); + } else { + navigate(-1); } }, });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedRecruitmentForm/index.tsx` around lines 84 - 91, The onSuccess handler for updateQuestions only acts when location.state?.enableAfterSave is true, leaving normal saves without feedback; modify the updateQuestions onSuccess callback in ManagedRecruitmentForm so that when enableAfterSave is falsy it still provides user feedback (e.g., show a success toast and/or call navigate(-1)); update the onSuccess to branch: if enableAfterSave then call patchSettings(...) as before, else trigger the chosen feedback (toast success and/or navigate) so users receive confirmation after a regular save.
♻️ Duplicate comments (5)
src/pages/Manager/ManagedApplicationList/index.tsx (2)
130-131:⚠️ Potential issue | 🟡 Minor유효하지 않은 색상 토큰
text-text-600
text-text-600은 theme.css에 정의되지 않은 클래스입니다.text-indigo-600등 유효한 토큰으로 교체하세요.🎨 수정 제안
- <span className="text-text-600 text-[15px] leading-6 font-semibold"> + <span className="text-indigo-600 text-[15px] leading-6 font-semibold">🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedApplicationList/index.tsx` around lines 130 - 131, The span in ManagedApplicationList rendering uses an invalid Tailwind token "text-text-600"; update the className on the span (the element that renders 대기중 {managedClubApplicationList?.totalCount ?? 0}명) to use a valid theme token such as "text-indigo-600" (or whichever valid color token your theme defines) so the styling applies correctly.
40-43:⚠️ Potential issue | 🟡 Minor클릭 가능한
div에 키보드 접근성 필요
onClick만 있고role,tabIndex,onKeyDown이 없어 키보드 사용자가 접근할 수 없습니다.♿ 접근성 개선 제안
<div + role="button" + tabIndex={0} className="border-indigo-5 active:bg-indigo-5/60 flex cursor-pointer items-center justify-between rounded-2xl border bg-white p-3" onClick={() => onDetail(application.id)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onDetail(application.id); + } + }} >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedApplicationList/index.tsx` around lines 40 - 43, The clickable div using onClick={() => onDetail(application.id)} lacks keyboard accessibility; update the element to behave like a button by adding role="button", tabIndex={0}, and an onKeyDown handler that calls onDetail(application.id) when Enter or Space is pressed (handling event.preventDefault() for Space), ensuring the existing onClick remains for mouse users; reference the div containing onClick and the onDetail(application.id) call to locate where to add these attributes and the handler.src/pages/Manager/ManagedRecruitmentWrite/index.tsx (1)
181-196:⚠️ Potential issue | 🟠 Majorblob URL 정리가 unmount 경로에는 없습니다.
삭제 시
revokeObjectURL은 추가되었지만, 컴포넌트 unmount 시 생성된 blob URL들이 정리되지 않아 메모리 누수가 발생할 수 있습니다.♻️ 수정 제안
+ const imagesRef = useRef<ImageItem[]>([]); + + useEffect(() => { + imagesRef.current = images; + }, [images]); + + useEffect(() => { + return () => { + imagesRef.current.forEach((image) => { + if (!image.isExisting) { + URL.revokeObjectURL(image.previewUrl); + } + }); + }; + }, []);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedRecruitmentWrite/index.tsx` around lines 181 - 196, The component never revokes blob preview URLs on unmount, causing potential memory leaks; add a cleanup that revokes any created preview URLs. Create a ref (e.g., imagesRef) that you update whenever images changes, and add a useEffect with a cleanup function that iterates imagesRef.current and calls URL.revokeObjectURL(image.previewUrl) for each image where image.previewUrl exists and image.isExisting is false; keep the existing revoke in handleDeleteImage but ensure the unmount cleanup covers remaining blob URLs.src/pages/User/MyPage/components/UserInfoCard.tsx (2)
100-100:⚠️ Potential issue | 🟠 Major클릭 가능한 div에 키보드 접근성 필요
isClickable일 때 마우스만 지원되고 키보드 사용자는 접근 불가합니다.role="button",tabIndex={0},onKeyDown추가가 필요합니다.🛠️ 수정 제안
+ const handleCardKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => { + if (!isClickable) return; + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleCardClick(); + } + }; return ( - <div className={cardClassName} onClick={isClickable ? handleCardClick : undefined}> + <div + className={cardClassName} + onClick={isClickable ? handleCardClick : undefined} + role={isClickable ? 'button' : undefined} + tabIndex={isClickable ? 0 : undefined} + onKeyDown={isClickable ? handleCardKeyDown : undefined} + >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/User/MyPage/components/UserInfoCard.tsx` at line 100, The clickable container currently only supports mouse clicks; when isClickable is true, add keyboard accessibility by setting role="button" and tabIndex={0} on the div (the element using cardClassName) and implement an onKeyDown handler that listens for Enter and Space and invokes handleCardClick (for Space also preventDefault to avoid page scroll). Keep the existing onClick behavior and ensure the onKeyDown is only attached when isClickable is true.
2-2:⚠️ Potential issue | 🟡 Minor아이콘 변수명과 SVG 파일명 불일치
RightArrowIcon이Chevron-left-dark.svg를 import하고 있어 실제 화살표 방향과 변수명이 맞지 않습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/User/MyPage/components/UserInfoCard.tsx` at line 2, The imported icon variable RightArrowIcon does not match the SVG file Chevron-left-dark.svg; update the import in UserInfoCard.tsx so the variable name matches the asset (e.g., import LeftChevronIcon from '@/assets/svg/Chevron-left-dark.svg') or swap to the correct SVG file for a right arrow (e.g., import RightArrowIcon from '@/assets/svg/Chevron-right-dark.svg'); ensure the component uses the renamed symbol (RightArrowIcon or LeftChevronIcon) consistently.
🧹 Nitpick comments (10)
src/pages/Manager/ManagedApplicationList/index.tsx (1)
47-52: 인라인 폰트 스타일 대신 타이포그래피 토큰 고려
text-[15px],text-[13px]대신text-sub1,text-body2등 정의된 타이포그래피 토큰 사용을 권장합니다. 디자인 시스템 일관성 유지에 도움이 됩니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedApplicationList/index.tsx` around lines 47 - 52, Replace the inline font-size classes on the application entries with the design system typography tokens: change the className that contains "text-[15px] leading-6 font-semibold" for the title to use the token (e.g., text-sub1 or whichever token maps to sub1) and change the className that contains "text-[13px] leading-[1.6] font-medium" for the appliedAt line to use the body token (e.g., text-body2). Update the JSX around the elements referencing application.name, application.studentNumber and formatIsoDateToYYYYMMDDHHMM(application.appliedAt) to use those tokens so styling follows the typography system and remove the explicit pixel-based text-* classes.src/pages/Manager/ManagedClubList/index.tsx (1)
2-2: SVG 파일명과 실제 방향이 불일치합니다.
Chevron-left-dark.svg를RightArrowIcon으로 사용하고 있습니다. SVG 경로상 실제로 오른쪽 방향 화살표이지만, 파일명이 혼란을 줄 수 있습니다. 에셋 파일명 정리를 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedClubList/index.tsx` at line 2, The imported asset name and variable are inconsistent: you import "Chevron-left-dark.svg" as RightArrowIcon; update either the asset filename or the import/identifier so names match the arrow direction—e.g., rename the SVG file to "Chevron-right-dark.svg" or change the import to import LeftArrowIcon from '@/assets/svg/Chevron-left-dark.svg' and update all usages of RightArrowIcon in ManagedClubList to the new identifier to keep asset names and variable names consistent.src/pages/Manager/ManagedRecruitmentWrite/index.tsx (2)
469-480: 이미지 인디케이터 버튼에 고유 key로 index 사용 중입니다.이미지 순서가 변경되거나 삭제될 때 React 렌더링 이슈가 발생할 수 있습니다. 가능하다면
previewUrl이나 고유 식별자를 key로 사용하세요.♻️ 수정 제안
- {images.map((_, index) => ( - <button - key={index} + {images.map((img, index) => ( + <button + key={img.previewUrl}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedRecruitmentWrite/index.tsx` around lines 469 - 480, The image indicator uses the array index as the React key in the images.map(...) rendering which can cause incorrect reconciliation when images are reordered or removed; update the key to a stable unique identifier such as each image's previewUrl or an id property (e.g., use image.previewUrl or image.id) instead of index, keeping the rest of the handler (onClick={() => setCurrentImageIndex(index)}) and currentImageIndex logic unchanged so buttons still set the correct image by index.
3-3:twMerge대신cn유틸리티 사용을 권장합니다.
ManagedRecruitmentForm과 동일하게, 프로젝트 전체에서cn유틸리티로 통일하면 일관성이 높아집니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedRecruitmentWrite/index.tsx` at line 3, Replace the twMerge import and all its usages in this file with the project's cn utility to match ManagedRecruitmentForm; specifically remove "twMerge" and import "cn" instead, then update any calls to twMerge(...) to cn(...), preserving the same className concatenation semantics and any conditional class logic so styling behaviour remains identical.src/pages/Manager/ManagedRecruitmentForm/index.tsx (1)
3-9:twMerge와cn둘 다 import하고 있습니다.
cn유틸리티가 이미clsx와tailwind-merge를 결합한 함수입니다. 일관성을 위해cn만 사용하는 것을 권장합니다.♻️ 수정 제안
- import { twMerge } from 'tailwind-merge'; ... - <div className={twMerge(sectionCardStyle, 'items-center py-10 text-center')}> + <div className={cn(sectionCardStyle, 'items-center py-10 text-center')}>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedRecruitmentForm/index.tsx` around lines 3 - 9, The file currently imports both twMerge and cn (top-level import of twMerge and the cn utility); remove the redundant twMerge import and replace any uses of twMerge in this module with the unified cn helper so only cn is imported/used (update the import statement to drop twMerge and modify occurrences of twMerge(...) to cn(...) and ensure types/usages tied to useManagedClubQuestions/useManagedClubQuestionsMutation remain unchanged).src/pages/Manager/ManagedClubProfile/index.tsx (1)
38-39:clubId가undefined일 경우NaN처리가 필요합니다.
useParams에서clubId가 없을 경우Number(undefined)는NaN을 반환하여 API 호출이 실패할 수 있습니다.♻️ 수정 제안
const { clubId } = useParams<{ clubId: string }>(); - const numericClubId = Number(clubId); + const numericClubId = Number(clubId) || 0; + + if (!numericClubId) { + return <div>잘못된 접근입니다.</div>; + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedClubProfile/index.tsx` around lines 38 - 39, Compute and validate the route param before calling the data hook: convert clubId to a number and if it's invalid set numericClubId to undefined (e.g., const numericClubId = Number.isFinite(Number(clubId)) ? Number(clubId) : undefined), then ensure useGetClubDetail is invoked in a way that skips the API when numericClubId is undefined (for example by passing numericClubId to useGetClubDetail only if defined or using the hook's "enabled" flag). Update references to numericClubId, clubId, and useGetClubDetail so the API is not called with NaN.src/pages/Manager/ManagedMemberList/index.tsx (2)
474-475: 하드코딩된 색상값 정리 권장
#69BFDF,#A5B3C1등 하드코딩된 색상이 여러 곳에 반복됩니다.theme.css의 색상 토큰으로 통일하면 유지보수성이 향상됩니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedMemberList/index.tsx` around lines 474 - 475, The component currently hardcodes colors like "#69BFDF" and "#A5B3C1" inside the className string in ManagedMemberList (see the element with className="... bg-[`#69BFDF`] ..." and other occurrences); update these to use the centralized theme color tokens defined in theme.css (replace the hex values with the appropriate CSS variable or token names used by the project, e.g., var(--color-XXX) or the existing token class names) so all instances in ManagedMemberList/index.tsx refer to theme tokens instead of literal hex codes; search for "#69BFDF" and "#A5B3C1" in this file, replace them with the corresponding theme variables, and ensure theme.css contains those tokens (add them if missing) so styling remains consistent and maintainable.
57-103: RoleManageSelector 드롭다운에 접근성 속성 누락드롭다운 버튼과 옵션 목록에 ARIA 속성이 없어 스크린 리더 사용자가 상태를 알 수 없습니다.
♻️ 접근성 개선 제안
<button type="button" onClick={() => setIsOpen((prev) => !prev)} + aria-haspopup="listbox" + aria-expanded={isOpen} className="flex h-[29px] min-w-[72px] items-center rounded-full border border-[`#A5B3C1`] bg-white pr-2 pl-[18px]" >- <div className="absolute top-[31px] left-0 z-10 w-[72px] overflow-hidden rounded-[10px] border border-[`#A5B3C1`] bg-white"> + <div + role="listbox" + aria-label="직책 선택" + className="absolute top-[31px] left-0 z-10 w-[72px] overflow-hidden rounded-[10px] border border-[`#A5B3C1`] bg-white" + > {ROLE_OPTIONS.map((option, index) => ( <button key={option.value} type="button" + role="option" + aria-selected={option.value === value}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedMemberList/index.tsx` around lines 57 - 103, The RoleManageSelector component lacks ARIA attributes and keyboard focus semantics; update RoleManageSelector so the toggle button includes aria-haspopup="menu", an id, and aria-expanded tied to isOpen, and make the options container use role="menu" with an id that the button references (aria-controls); render each option button with role="menuitem", a unique id, and tabIndex={-1} (or manage focus) and ensure clicking an option calls onChange and closes the menu as already implemented; also ensure useClickTouchOutside still closes the menu and add keyboard handling on the toggle and option list (Escape to close, ArrowUp/ArrowDown to move focus) so screen readers and keyboard users can perceive and operate the dropdown.src/pages/Manager/ManagedApplicationDetail/index.tsx (2)
53-117: 페이지 엔트리는Layout으로 감싸는 편이 맞습니다.지금은 fragment를 바로 반환해서 이 페이지에서
showBottomNav와contentClassName을 명시적으로 제어하지 못합니다. 관리자 페이지 엔트리라면Layout을 통해 배경/하단 네비 노출을 함께 맞춰주세요.Based on learnings,
src/pages/**/*.tsx에서는Layout에showBottomNav와contentClassName를 전달해야 합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedApplicationDetail/index.tsx` around lines 53 - 117, Wrap the returned fragment in the Layout component and forward showBottomNav and contentClassName so the page can control background and bottom navigation; replace the top-level <>...</> with <Layout showBottomNav={false} contentClassName="your-content-class"> ...children... </Layout>, keeping ApplicationDetailContent, the two BottomModal blocks and their handlers (openApprove/closeApprove, openReject/closeReject, handleApprove, handleReject, isApproving/isRejecting/isPending, application) intact.
13-18: 버튼 클래스는 테마 토큰으로 맞춰주세요.Line 13의
#69BFDF,text-[16px],leading-[22px],tracking-[-0.408px]는 공용 color/typography token을 우회합니다. 이 상수는 여러 버튼이 같이 쓰는 만큼 지금 토큰 기반으로 맞춰두는 편이 이후 리디자인 유지보수에 안전합니다.As per coding guidelines,
**/*.{ts,tsx}에서는src/styles/theme.css의 color token과 typography token을 우선 사용해야 합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedApplicationDetail/index.tsx` around lines 13 - 18, Replace hardcoded color and typography values in the button class constants with your theme tokens: update BUTTON_BASE_CLASS to use the theme typography tokens instead of text-[16px], leading-[22px], tracking-[-0.408px], and update BUTTON_PRIMARY_CLASS and BUTTON_SECONDARY_CLASS to use the theme color token(s) instead of `#69BFDF`; ensure BUTTON_DISABLED_CLASS / BUTTON_DISABLED_WITH_CURSOR_CLASS keep only state rules and use token-aware styles for opacity/cursor if available. Locate and modify the constants BUTTON_BASE_CLASS, BUTTON_SECONDARY_CLASS, BUTTON_PRIMARY_CLASS, and BUTTON_DISABLED_WITH_CURSOR_CLASS to reference the project's color/typography tokens (CSS variable or token utility classes) rather than hardcoded values.
🤖 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/pages/Manager/ManagedApplicationDetail/index.tsx`:
- Around line 54-56: The image modal Portal inside ApplicationDetailContent
conflicts with the BottomModal overlay (both using z-100), causing
nondeterministic stacking; update the BottomModal usage in this file (and the
similar usages in the 78-116 block) to avoid the clash by either
programmatically closing the image modal before opening the confirmation modal
(call the ApplicationDetailContent image-modal close handler when triggering
BottomModal) or by passing a distinct overlayClassName to BottomModal that sets
a higher z-index (e.g., z-200) so the confirm modal reliably stacks above the
image Portal; make the change where ApplicationDetailContent and BottomModal are
composed to ensure deterministic layering.
In `@src/pages/Manager/ManagedApplicationList/index.tsx`:
- Line 62: The hardcoded color `#69BFDF` must be moved into the theme tokens and
replaced with a token usage; add a token like --color-blue-400: `#69BFDF` to
theme.css (or the project's theme token file) and create a corresponding
utility/class (e.g., text-blue-400) then update occurrences such as the
className in ManagedApplicationList (the element with className "flex h-6 w-6
items-center justify-center text-[`#69BFDF`]") and other files (ManagedMemberList,
ManagedApplicationDetail) to use the new token/utility instead of the hex
literal so the color is consistent across the app.
In `@src/pages/Manager/ManagedClubProfile/index.tsx`:
- Around line 60-67: The effect currently resets local edit state whenever
initialDescription/initialImageUrl/... change (e.g., React Query background
refetch), which can wipe user edits; modify the useEffect so it only initializes
state on first mount or when the club identity changes (not on every refetch):
replace the dependency list of useEffect to a stable identifier such as clubId
(or use an isFirstLoad ref) and/or guard the setters (setDescription,
setLocation, setIntroduce, setImageFile, setImagePreview) so they run only when
no user edits exist (e.g., only if current local state is empty or a mountedRef
is false); keep clearLocalPreviewUrl(localPreviewUrlRef) behavior but ensure it
is invoked only on initial load/club change to avoid clearing in-flight edits.
In `@src/pages/Manager/ManagedMemberList/index.tsx`:
- Around line 415-421: The current code fires two Promise.all calls for
promoteUserIds/demoteUserIds and always shows a success toast and closes the
modal even if some requests fail; wrap these operations in error handling
(either use Promise.allSettled for both promoteUserIds and demoteUserIds or put
the Promise.all calls inside a try/catch) and check results: if any
changeMemberPosition call failed, call showToast with an error message (and do
not call closeRoleManage or show the success toast), otherwise on full success
call invalidate via queryClient.invalidateQueries({ queryKey:
memberQueryKeys.managedMembers(clubId) }) and show the success toast then
closeRoleManage; ensure failures are surfaced to the user and successes still
trigger cache invalidation when appropriate.
In `@src/pages/User/MyPage/components/UserInfoCard.tsx`:
- Around line 27-29: The Card is currently a plain div receiving onClick so
keyboard users cannot activate it; update the Card component (where it accepts
onClick) to either render a native button element or add keyboard accessibility
props when rendering a focusable div: set role="button", tabIndex={0}, and
implement onKeyDown to invoke the same handler as onClick for Enter and Space
(ensure you call the provided onClick handler, e.g., handleClick), and add any
appropriate ARIA attributes (aria-pressed/aria-label) if the Card represents a
toggle or actionable control; alternatively replace usage in UserInfoCard.tsx
with a semantic <button> if that better fits the visual/layout needs.
---
Outside diff comments:
In `@src/pages/Manager/ManagedRecruitmentForm/index.tsx`:
- Around line 84-91: The onSuccess handler for updateQuestions only acts when
location.state?.enableAfterSave is true, leaving normal saves without feedback;
modify the updateQuestions onSuccess callback in ManagedRecruitmentForm so that
when enableAfterSave is falsy it still provides user feedback (e.g., show a
success toast and/or call navigate(-1)); update the onSuccess to branch: if
enableAfterSave then call patchSettings(...) as before, else trigger the chosen
feedback (toast success and/or navigate) so users receive confirmation after a
regular save.
---
Duplicate comments:
In `@src/pages/Manager/ManagedApplicationList/index.tsx`:
- Around line 130-131: The span in ManagedApplicationList rendering uses an
invalid Tailwind token "text-text-600"; update the className on the span (the
element that renders 대기중 {managedClubApplicationList?.totalCount ?? 0}명) to use
a valid theme token such as "text-indigo-600" (or whichever valid color token
your theme defines) so the styling applies correctly.
- Around line 40-43: The clickable div using onClick={() =>
onDetail(application.id)} lacks keyboard accessibility; update the element to
behave like a button by adding role="button", tabIndex={0}, and an onKeyDown
handler that calls onDetail(application.id) when Enter or Space is pressed
(handling event.preventDefault() for Space), ensuring the existing onClick
remains for mouse users; reference the div containing onClick and the
onDetail(application.id) call to locate where to add these attributes and the
handler.
In `@src/pages/Manager/ManagedRecruitmentWrite/index.tsx`:
- Around line 181-196: The component never revokes blob preview URLs on unmount,
causing potential memory leaks; add a cleanup that revokes any created preview
URLs. Create a ref (e.g., imagesRef) that you update whenever images changes,
and add a useEffect with a cleanup function that iterates imagesRef.current and
calls URL.revokeObjectURL(image.previewUrl) for each image where
image.previewUrl exists and image.isExisting is false; keep the existing revoke
in handleDeleteImage but ensure the unmount cleanup covers remaining blob URLs.
In `@src/pages/User/MyPage/components/UserInfoCard.tsx`:
- Line 100: The clickable container currently only supports mouse clicks; when
isClickable is true, add keyboard accessibility by setting role="button" and
tabIndex={0} on the div (the element using cardClassName) and implement an
onKeyDown handler that listens for Enter and Space and invokes handleCardClick
(for Space also preventDefault to avoid page scroll). Keep the existing onClick
behavior and ensure the onKeyDown is only attached when isClickable is true.
- Line 2: The imported icon variable RightArrowIcon does not match the SVG file
Chevron-left-dark.svg; update the import in UserInfoCard.tsx so the variable
name matches the asset (e.g., import LeftChevronIcon from
'@/assets/svg/Chevron-left-dark.svg') or swap to the correct SVG file for a
right arrow (e.g., import RightArrowIcon from
'@/assets/svg/Chevron-right-dark.svg'); ensure the component uses the renamed
symbol (RightArrowIcon or LeftChevronIcon) consistently.
---
Nitpick comments:
In `@src/pages/Manager/ManagedApplicationDetail/index.tsx`:
- Around line 53-117: Wrap the returned fragment in the Layout component and
forward showBottomNav and contentClassName so the page can control background
and bottom navigation; replace the top-level <>...</> with <Layout
showBottomNav={false} contentClassName="your-content-class"> ...children...
</Layout>, keeping ApplicationDetailContent, the two BottomModal blocks and
their handlers (openApprove/closeApprove, openReject/closeReject, handleApprove,
handleReject, isApproving/isRejecting/isPending, application) intact.
- Around line 13-18: Replace hardcoded color and typography values in the button
class constants with your theme tokens: update BUTTON_BASE_CLASS to use the
theme typography tokens instead of text-[16px], leading-[22px],
tracking-[-0.408px], and update BUTTON_PRIMARY_CLASS and BUTTON_SECONDARY_CLASS
to use the theme color token(s) instead of `#69BFDF`; ensure BUTTON_DISABLED_CLASS
/ BUTTON_DISABLED_WITH_CURSOR_CLASS keep only state rules and use token-aware
styles for opacity/cursor if available. Locate and modify the constants
BUTTON_BASE_CLASS, BUTTON_SECONDARY_CLASS, BUTTON_PRIMARY_CLASS, and
BUTTON_DISABLED_WITH_CURSOR_CLASS to reference the project's color/typography
tokens (CSS variable or token utility classes) rather than hardcoded values.
In `@src/pages/Manager/ManagedApplicationList/index.tsx`:
- Around line 47-52: Replace the inline font-size classes on the application
entries with the design system typography tokens: change the className that
contains "text-[15px] leading-6 font-semibold" for the title to use the token
(e.g., text-sub1 or whichever token maps to sub1) and change the className that
contains "text-[13px] leading-[1.6] font-medium" for the appliedAt line to use
the body token (e.g., text-body2). Update the JSX around the elements
referencing application.name, application.studentNumber and
formatIsoDateToYYYYMMDDHHMM(application.appliedAt) to use those tokens so
styling follows the typography system and remove the explicit pixel-based text-*
classes.
In `@src/pages/Manager/ManagedClubList/index.tsx`:
- Line 2: The imported asset name and variable are inconsistent: you import
"Chevron-left-dark.svg" as RightArrowIcon; update either the asset filename or
the import/identifier so names match the arrow direction—e.g., rename the SVG
file to "Chevron-right-dark.svg" or change the import to import LeftArrowIcon
from '@/assets/svg/Chevron-left-dark.svg' and update all usages of
RightArrowIcon in ManagedClubList to the new identifier to keep asset names and
variable names consistent.
In `@src/pages/Manager/ManagedClubProfile/index.tsx`:
- Around line 38-39: Compute and validate the route param before calling the
data hook: convert clubId to a number and if it's invalid set numericClubId to
undefined (e.g., const numericClubId = Number.isFinite(Number(clubId)) ?
Number(clubId) : undefined), then ensure useGetClubDetail is invoked in a way
that skips the API when numericClubId is undefined (for example by passing
numericClubId to useGetClubDetail only if defined or using the hook's "enabled"
flag). Update references to numericClubId, clubId, and useGetClubDetail so the
API is not called with NaN.
In `@src/pages/Manager/ManagedMemberList/index.tsx`:
- Around line 474-475: The component currently hardcodes colors like "#69BFDF"
and "#A5B3C1" inside the className string in ManagedMemberList (see the element
with className="... bg-[`#69BFDF`] ..." and other occurrences); update these to
use the centralized theme color tokens defined in theme.css (replace the hex
values with the appropriate CSS variable or token names used by the project,
e.g., var(--color-XXX) or the existing token class names) so all instances in
ManagedMemberList/index.tsx refer to theme tokens instead of literal hex codes;
search for "#69BFDF" and "#A5B3C1" in this file, replace them with the
corresponding theme variables, and ensure theme.css contains those tokens (add
them if missing) so styling remains consistent and maintainable.
- Around line 57-103: The RoleManageSelector component lacks ARIA attributes and
keyboard focus semantics; update RoleManageSelector so the toggle button
includes aria-haspopup="menu", an id, and aria-expanded tied to isOpen, and make
the options container use role="menu" with an id that the button references
(aria-controls); render each option button with role="menuitem", a unique id,
and tabIndex={-1} (or manage focus) and ensure clicking an option calls onChange
and closes the menu as already implemented; also ensure useClickTouchOutside
still closes the menu and add keyboard handling on the toggle and option list
(Escape to close, ArrowUp/ArrowDown to move focus) so screen readers and
keyboard users can perceive and operate the dropdown.
In `@src/pages/Manager/ManagedRecruitmentForm/index.tsx`:
- Around line 3-9: The file currently imports both twMerge and cn (top-level
import of twMerge and the cn utility); remove the redundant twMerge import and
replace any uses of twMerge in this module with the unified cn helper so only cn
is imported/used (update the import statement to drop twMerge and modify
occurrences of twMerge(...) to cn(...) and ensure types/usages tied to
useManagedClubQuestions/useManagedClubQuestionsMutation remain unchanged).
In `@src/pages/Manager/ManagedRecruitmentWrite/index.tsx`:
- Around line 469-480: The image indicator uses the array index as the React key
in the images.map(...) rendering which can cause incorrect reconciliation when
images are reordered or removed; update the key to a stable unique identifier
such as each image's previewUrl or an id property (e.g., use image.previewUrl or
image.id) instead of index, keeping the rest of the handler (onClick={() =>
setCurrentImageIndex(index)}) and currentImageIndex logic unchanged so buttons
still set the correct image by index.
- Line 3: Replace the twMerge import and all its usages in this file with the
project's cn utility to match ManagedRecruitmentForm; specifically remove
"twMerge" and import "cn" instead, then update any calls to twMerge(...) to
cn(...), preserving the same className concatenation semantics and any
conditional class logic so styling behaviour remains identical.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 401d5c37-8d62-452c-ae23-657d3168cc6c
📒 Files selected for processing (16)
src/components/common/BottomModal.tsxsrc/components/layout/Header/components/ManagerHeader.tsxsrc/components/layout/Header/constants.tssrc/components/layout/Header/routeTitles.tssrc/components/layout/index.tsxsrc/pages/Manager/ManagedApplicationDetail/index.tsxsrc/pages/Manager/ManagedApplicationList/index.tsxsrc/pages/Manager/ManagedClubList/index.tsxsrc/pages/Manager/ManagedClubProfile/index.tsxsrc/pages/Manager/ManagedMemberApplicationDetail/index.tsxsrc/pages/Manager/ManagedMemberList/index.tsxsrc/pages/Manager/ManagedRecruitment/index.tsxsrc/pages/Manager/ManagedRecruitmentForm/index.tsxsrc/pages/Manager/ManagedRecruitmentWrite/index.tsxsrc/pages/Manager/hooks/useManagedMembers.tssrc/pages/User/MyPage/components/UserInfoCard.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
- src/pages/Manager/ManagedMemberApplicationDetail/index.tsx
- src/components/layout/Header/components/ManagerHeader.tsx
- src/pages/Manager/hooks/useManagedMembers.ts
- src/components/layout/Header/routeTitles.ts
- src/components/layout/index.tsx
- src/components/common/BottomModal.tsx
| <ApplicationDetailContent | ||
| application={application} | ||
| footer={ |
There was a problem hiding this comment.
이미지 모달과 확인 모달의 레이어가 충돌할 수 있어요.
ApplicationDetailContent 쪽 이미지 Portal과 여기 BottomModal 오버레이가 둘 다 z-100이라, 두 상태가 겹치면 어느 쪽이 위에 뜰지 비결정적입니다. 확인 모달을 열 때 이미지 모달을 닫거나 overlayClassName으로 z-index를 분리하는 쪽이 안전합니다.
Also applies to: 78-116
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/Manager/ManagedApplicationDetail/index.tsx` around lines 54 - 56,
The image modal Portal inside ApplicationDetailContent conflicts with the
BottomModal overlay (both using z-100), causing nondeterministic stacking;
update the BottomModal usage in this file (and the similar usages in the 78-116
block) to avoid the clash by either programmatically closing the image modal
before opening the confirmation modal (call the ApplicationDetailContent
image-modal close handler when triggering BottomModal) or by passing a distinct
overlayClassName to BottomModal that sets a higher z-index (e.g., z-200) so the
confirm modal reliably stacks above the image Portal; make the change where
ApplicationDetailContent and BottomModal are composed to ensure deterministic
layering.
| onClick={(e) => onApprove(e, application.id)} | ||
| disabled={disabled} | ||
| aria-label={`${application.name} 지원 승인`} | ||
| className="flex h-6 w-6 items-center justify-center text-[#69BFDF] disabled:opacity-50" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# theme.css에서 blue 또는 cyan 계열 토큰 확인
rg -n "blue|cyan|69BFDF" src/styles/theme.cssRepository: BCSDLab/KONECT_FRONT_END
Length of output: 131
🏁 Script executed:
# Get theme.css to see all color tokens
wc -l src/styles/theme.cssRepository: BCSDLab/KONECT_FRONT_END
Length of output: 91
🏁 Script executed:
# Search for more color tokens in theme.css
rg -n "color-" src/styles/theme.css | head -30Repository: BCSDLab/KONECT_FRONT_END
Length of output: 665
🏁 Script executed:
# Look at the specific file and context around line 62
sed -n '55,70p' src/pages/Manager/ManagedApplicationList/index.tsxRepository: BCSDLab/KONECT_FRONT_END
Length of output: 613
🏁 Script executed:
# Search for `#69BFDF` anywhere in the codebase
rg -r "#69BFDF|69BFDF" --type tsx --type ts --type cssRepository: BCSDLab/KONECT_FRONT_END
Length of output: 464
🏁 Script executed:
# Read full theme.css to see all tokens
cat -n src/styles/theme.cssRepository: BCSDLab/KONECT_FRONT_END
Length of output: 1172
🏁 Script executed:
# Search for `#69BFDF` usage properly
rg "69BFDF" src/Repository: BCSDLab/KONECT_FRONT_END
Length of output: 2535
#69BFDF 색상을 테마 토큰으로 정의하세요
#69BFDF는 theme.css에 정의되지 않은 색상입니다. 이 색상이 설계 시스템의 일부라면 --color-blue-400: #69BFDF``로 theme.css에 추가하고, text-blue-400 등의 토큰으로 사용하세요. 현재 이 색상이 ManagedMemberList, ManagedApplicationDetail 등 여러 파일에서 하드코딩되어 있어 일관성을 위해 테마 토큰화가 필요합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/Manager/ManagedApplicationList/index.tsx` at line 62, The hardcoded
color `#69BFDF` must be moved into the theme tokens and replaced with a token
usage; add a token like --color-blue-400: `#69BFDF` to theme.css (or the project's
theme token file) and create a corresponding utility/class (e.g., text-blue-400)
then update occurrences such as the className in ManagedApplicationList (the
element with className "flex h-6 w-6 items-center justify-center
text-[`#69BFDF`]") and other files (ManagedMemberList, ManagedApplicationDetail)
to use the new token/utility instead of the hex literal so the color is
consistent across the app.
| useEffect(() => { | ||
| clearLocalPreviewUrl(localPreviewUrlRef); | ||
| setDescription(initialDescription); | ||
| setLocation(initialLocation); | ||
| setIntroduce(initialIntroduce); | ||
| setImageFile(null); | ||
| setImagePreview(initialImageUrl); | ||
| }, [initialDescription, initialImageUrl, initialIntroduce, initialLocation]); |
There was a problem hiding this comment.
clubDetail 재조회 시 사용자 입력이 초기화될 수 있습니다.
이 useEffect는 initialDescription, initialImageUrl 등이 변경될 때마다 실행됩니다. React Query의 background refetch로 인해 사용자가 편집 중인 내용이 의도치 않게 리셋될 수 있습니다.
♻️ 수정 제안
+ const hasInitialized = useRef(false);
+
useEffect(() => {
+ if (hasInitialized.current) return;
+ hasInitialized.current = true;
+
clearLocalPreviewUrl(localPreviewUrlRef);
setDescription(initialDescription);
setLocation(initialLocation);
setIntroduce(initialIntroduce);
setImageFile(null);
setImagePreview(initialImageUrl);
}, [initialDescription, initialImageUrl, initialIntroduce, initialLocation]);📝 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.
| useEffect(() => { | |
| clearLocalPreviewUrl(localPreviewUrlRef); | |
| setDescription(initialDescription); | |
| setLocation(initialLocation); | |
| setIntroduce(initialIntroduce); | |
| setImageFile(null); | |
| setImagePreview(initialImageUrl); | |
| }, [initialDescription, initialImageUrl, initialIntroduce, initialLocation]); | |
| const hasInitialized = useRef(false); | |
| useEffect(() => { | |
| if (hasInitialized.current) return; | |
| hasInitialized.current = true; | |
| clearLocalPreviewUrl(localPreviewUrlRef); | |
| setDescription(initialDescription); | |
| setLocation(initialLocation); | |
| setIntroduce(initialIntroduce); | |
| setImageFile(null); | |
| setImagePreview(initialImageUrl); | |
| }, [initialDescription, initialImageUrl, initialIntroduce, initialLocation]); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/Manager/ManagedClubProfile/index.tsx` around lines 60 - 67, The
effect currently resets local edit state whenever
initialDescription/initialImageUrl/... change (e.g., React Query background
refetch), which can wipe user edits; modify the useEffect so it only initializes
state on first mount or when the club identity changes (not on every refetch):
replace the dependency list of useEffect to a stable identifier such as clubId
(or use an isFirstLoad ref) and/or guard the setters (setDescription,
setLocation, setIntroduce, setImageFile, setImagePreview) so they run only when
no user edits exist (e.g., only if current local state is empty or a mountedRef
is false); keep clearLocalPreviewUrl(localPreviewUrlRef) behavior but ensure it
is invoked only on initial load/club change to avoid clearing in-flight edits.
| await Promise.all(promoteUserIds.map((userId) => changeMemberPosition({ userId, data: { position: 'MANAGER' } }))); | ||
|
|
||
| await Promise.all(demoteUserIds.map((userId) => changeMemberPosition({ userId, data: { position: 'MEMBER' } }))); | ||
|
|
||
| await queryClient.invalidateQueries({ queryKey: memberQueryKeys.managedMembers(clubId) }); | ||
| showToast('직책이 변경되었습니다'); | ||
| closeRoleManage(); |
There was a problem hiding this comment.
Promise.all 에러 처리 누락
여러 멤버 직책 변경 중 하나라도 실패하면, 사용자는 모르고 성공 토스트만 보게 됩니다.
🐛 에러 처리 추가 제안
+ try {
await Promise.all(promoteUserIds.map((userId) => changeMemberPosition({ userId, data: { position: 'MANAGER' } })));
await Promise.all(demoteUserIds.map((userId) => changeMemberPosition({ userId, data: { position: 'MEMBER' } })));
await queryClient.invalidateQueries({ queryKey: memberQueryKeys.managedMembers(clubId) });
showToast('직책이 변경되었습니다');
+ } catch {
+ showToast('직책 변경 중 오류가 발생했습니다');
+ }
closeRoleManage();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/Manager/ManagedMemberList/index.tsx` around lines 415 - 421, The
current code fires two Promise.all calls for promoteUserIds/demoteUserIds and
always shows a success toast and closes the modal even if some requests fail;
wrap these operations in error handling (either use Promise.allSettled for both
promoteUserIds and demoteUserIds or put the Promise.all calls inside a
try/catch) and check results: if any changeMemberPosition call failed, call
showToast with an error message (and do not call closeRoleManage or show the
success toast), otherwise on full success call invalidate via
queryClient.invalidateQueries({ queryKey: memberQueryKeys.managedMembers(clubId)
}) and show the success toast then closeRoleManage; ensure failures are surfaced
to the user and successes still trigger cache invalidation when appropriate.
| <Card | ||
| onClick={handleClick} | ||
| className="active:bg-indigo-5/50 cursor-pointer rounded-2xl border-0 px-3 py-[21px] shadow-[0px_0px_3px_0px_rgba(0,0,0,0.15)]" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Card 컴포넌트의 onClick 처리 방식 확인
ast-grep --pattern $'function Card($$$) {
$$$
}'Repository: BCSDLab/KONECT_FRONT_END
Length of output: 637
Card 컴포넌트가 div로만 구현되어 있어 onClick 요소의 키보드 접근성이 불가능합니다.
Card는 단순 div에 onClick만 전달되며, 키보드 접근성 처리(role="button", tabIndex, onKeyDown)가 없습니다. 클릭 가능한 요소는 반드시 키보드로도 조작 가능해야 합니다.
Card컴포넌트에서 키보드 접근성 지원 추가하거나button요소 사용으로 변경
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/User/MyPage/components/UserInfoCard.tsx` around lines 27 - 29, The
Card is currently a plain div receiving onClick so keyboard users cannot
activate it; update the Card component (where it accepts onClick) to either
render a native button element or add keyboard accessibility props when
rendering a focusable div: set role="button", tabIndex={0}, and implement
onKeyDown to invoke the same handler as onClick for Enter and Space (ensure you
call the provided onClick handler, e.g., handleClick), and add any appropriate
ARIA attributes (aria-pressed/aria-label) if the Card represents a toggle or
actionable control; alternatively replace usage in UserInfoCard.tsx with a
semantic <button> if that better fits the visual/layout needs.
✨ 요약
😎 해결한 이슈
Summary by CodeRabbit
새로운 기능
UI/UX 개선