[feat] 구글 스프레드 시트 동기화 기능, 관리자 페이지 내 채팅 추가#267
Conversation
WalkthroughGoogle Sheets 동기화 기능(스프레드시트 URL 입력, 미리보기, 확인)과 관리자용 개별 채팅 흐름을 추가했습니다. 클럽 관련 API 타입/함수(putClubSheet, postClubSheetImportPreview, postClubSheetImportConfirm)와 React Query 키/뮤테이션을 도입했고, ManagedMemberList를 훅/컴포넌트로 대대적 리팩토링했습니다. 여러 모달(시트 임포트, 추가/삭제/역할 변경 등), 액션 팝업, 로딩 오버레이 및 미리보기 페이지가 새로 추가되었고 일부 이벤트 타입과 날짜 포매터 명칭을 정리했습니다. Possibly related PRs
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
관리자 페이지에서 (1) 구글 스프레드시트 기반 부원 동기화(등록 전 미리보기/확정) 흐름과 (2) 지원자/부원 목록에서 1:1 채팅 진입을 추가하고, 관련 관리자 UI/구조를 리팩토링한 PR입니다.
Changes:
- 관리자 > 부원 관리에 구글 시트 URL 등록 모달, 미리보기 화면, 등록 확정 API 연동 추가
- 관리자 > 지원자/부원 목록에서 채팅방 생성 후 채팅방으로 이동하는 액션 추가
- 관리자 페이지 UI 아이콘/카드 스타일 정리 및 일부 폼 submit 이벤트 타입 정리
Reviewed changes
Copilot reviewed 43 out of 53 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/ts/datetime/date.ts | ISO 날짜 포맷터 함수명 변경 및 사용처 업데이트 기반 제공 |
| src/pages/User/MyPage/index.tsx | 마이페이지 레이아웃 패딩 조정 |
| src/pages/User/MyPage/components/ManagerInfoCard.tsx | 관리자 상세 카드 UI 단순화/정보 노출 축소 |
| src/pages/Manager/ManagedSheetImportPreview/index.tsx | (신규) 시트 import 미리보기 + 멤버 선택/확정 등록 UI |
| src/pages/Manager/ManagedRecruitmentWrite/index.tsx | submit 이벤트 타입 변경 |
| src/pages/Manager/ManagedRecruitmentForm/index.tsx | submit 이벤트 타입 변경 |
| src/pages/Manager/ManagedRecruitment/index.tsx | 아이콘 교체 및 카드 행 UI 개선 |
| src/pages/Manager/ManagedMemberList/index.tsx | 부원 관리 화면을 훅/컴포넌트로 분리 + 채팅/시트 불러오기 추가 |
| src/pages/Manager/ManagedMemberList/hooks/useRoleManage.ts | (신규) 직책 변경 로직 훅으로 분리 |
| src/pages/Manager/ManagedMemberList/hooks/usePreMemberAction.ts | (신규) 사전등록 회원 액션/삭제 훅 분리 |
| src/pages/Manager/ManagedMemberList/hooks/useMemberSheetImport.ts | (신규) 구글 시트 URL 검증/등록 및 preview 라우팅 훅 |
| src/pages/Manager/ManagedMemberList/hooks/useMemberAction.ts | (신규) 부원 액션(지원서/삭제 등) 훅 분리 |
| src/pages/Manager/ManagedMemberList/hooks/useManagedMemberList.ts | (신규) 부원/사전회원 쿼리 + 채팅 생성 + 각 서브훅 결합 |
| src/pages/Manager/ManagedMemberList/hooks/useAddMember.ts | (신규) 사전등록 회원 추가 훅 분리 |
| src/pages/Manager/ManagedMemberList/components/RoleManageSelector.tsx | (신규) 직책 변경 대상 selector 컴포넌트 |
| src/pages/Manager/ManagedMemberList/components/RoleManageModal.tsx | (신규) 직책 변경 모달 컴포넌트 |
| src/pages/Manager/ManagedMemberList/components/RemoveMemberModal.tsx | (신규) 부원 삭제 확인 모달 |
| src/pages/Manager/ManagedMemberList/components/MemberSheetImportModal.tsx | (신규) 시트 URL 입력/가이드 모달 |
| src/pages/Manager/ManagedMemberList/components/MemberSheetImportLoadingOverlay.tsx | (신규) 시트 불러오기 로딩 오버레이 |
| src/pages/Manager/ManagedMemberList/components/MemberSection.tsx | (신규) 섹션 레이아웃 컴포넌트 |
| src/pages/Manager/ManagedMemberList/components/MemberCard.tsx | (신규) 부원 카드 컴포넌트 분리 |
| src/pages/Manager/ManagedMemberList/components/DeletePreMemberModal.tsx | (신규) 사전등록 삭제 확인 모달 |
| src/pages/Manager/ManagedMemberList/components/AddMemberModal.tsx | (신규) 사전등록 추가 모달 |
| src/pages/Manager/ManagedMemberList/components/ActionPopupMenu.tsx | (신규) 액션 팝업 메뉴 컴포넌트 |
| src/pages/Manager/ManagedClubDetail/index.tsx | 관리자 메뉴 아이콘/레이아웃 갱신 |
| src/pages/Manager/ManagedApplicationList/index.tsx | 지원자 리스트를 컴포넌트/훅으로 분리하고 채팅 액션 연결 |
| src/pages/Manager/ManagedApplicationList/hooks/useManagedApplicationActions.ts | (신규) 승인/거절/채팅/상세 이동 액션 훅 |
| src/pages/Manager/ManagedApplicationList/components/ApplicationCard.tsx | (신규) 지원자 카드 UI + 채팅 버튼 추가 |
| src/pages/Manager/hooks/useManagedMemberMutations.ts | 시트 upsert/confirm mutation 추가 + 직책 변경 mutation 시그니처 단순화 |
| src/pages/Manager/components/ApplicationDetailContent.tsx | 날짜 포맷 함수명 변경 반영 |
| src/pages/Club/ClubList/components/SearchBar.tsx | onSubmit 이벤트 타입 변경 |
| src/pages/Club/ClubDetail/components/ClubIntro.tsx | 반복 UI를 배열 map으로 리팩토링 |
| src/pages/Club/Application/index.tsx | submit 이벤트 타입 변경 |
| src/pages/Chat/ChatRoom.tsx | submit 이벤트 타입 변경 |
| src/pages/Auth/SignUp/FinishStep.tsx | Tailwind 유틸 단위로 스타일 값 정리 |
| src/index.css | iOS 스타일 스피너 keyframes 추가 |
| src/components/layout/Header/routeTitles.ts | 시트 preview 라우트 타이틀 매핑 추가 |
| src/components/layout/BottomNav/index.tsx | Tailwind 유틸로 치수/포지션 값 정리 |
| src/assets/svg/sms-icon.svg | (신규) 채팅 아이콘 |
| src/assets/svg/person-add-icon.svg | (신규) 인명부/추가 관련 아이콘 |
| src/assets/svg/ink-icon.svg | (신규) 지원자 관리 아이콘 |
| src/assets/svg/email-icon.svg | (신규) 지원서 아이콘 |
| src/assets/svg/drop-down-icon.svg | (신규) 드롭다운 아이콘 |
| src/assets/svg/close-icon.svg | (신규) 닫기 아이콘 |
| src/assets/svg/check-icon.svg | (신규) 체크 아이콘 |
| src/assets/svg/card-icon.svg | (신규) 회비 아이콘 |
| src/assets/svg/assignment-icon.svg | (신규) 모집 공고 아이콘 |
| src/assets/image/sheet-guide.png | (신규) 시트 가이드 이미지 |
| src/App.tsx | 시트 import preview 라우트 lazy import/route 추가 |
| src/apis/club/managedQueries.ts | 시트 import preview 쿼리 추가 |
| src/apis/club/managedMutations.ts | 시트 upsert/confirm mutation 추가 |
| src/apis/club/index.ts | 시트 upsert/preview/confirm API 함수 추가 |
| src/apis/club/entity.ts | 지원자 userId 추가 및 시트 import 관련 타입 추가 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <img className="h-12 w-12 rounded" src={managedClub?.imageUrl} alt={`${currentClub?.name} 동아리 사진`} /> | ||
| <div> | ||
| <div className="leading-[1.6] font-bold text-indigo-700">{currentClub?.name} 정보</div> | ||
| <div className="text-[11px] leading-3.75 font-medium text-indigo-300"> | ||
| {myInfo.studentNumber} / {myInfo.universityName} / {currentClub?.position} | ||
| </div> | ||
| </div> | ||
| <div className="leading-[1.6] font-bold text-indigo-700">{currentClub?.name} 정보</div> |
There was a problem hiding this comment.
currentClub가 없는 경우 alt가 "undefined 동아리 사진"처럼 렌더링될 수 있습니다. 접근성 측면에서 의미 있는 기본 alt(예: "동아리 사진")를 사용하거나, club 이름이 없을 때는 alt를 분기 처리하고 이미지가 없으면 placeholder/aria-hidden 처리도 고려해주세요.
| /> | ||
| ))} | ||
| </div> | ||
| <p className="text-text-500 py-1.25 text-[16px] leading-[1.6] font-semibold">인명부를 불러오는중..</p> |
There was a problem hiding this comment.
로딩 문구의 띄어쓰기/말줄임표가 어색합니다. 사용자에게 더 자연스럽게 보이도록 문구를 다듬어 주세요.
| 아래 인원이 부원으로 새로 등록될 예정이에요. | ||
| <br /> | ||
| 등록하기를 누르면 자동으로 등록 돼요. | ||
| </p> |
There was a problem hiding this comment.
문구의 띄어쓰기가 어색합니다. 한국어 맞춤법상 "등록돼요"처럼 붙여 쓰는 편이 자연스럽습니다.
| sheetImportPreview: (clubId: number) => | ||
| queryOptions({ | ||
| queryKey: managedClubQueryKeys.sheetImportPreview(clubId), | ||
| queryFn: () => postClubSheetImportPreview(clubId), | ||
| }), |
There was a problem hiding this comment.
sheetImportPreview가 POST 요청을 useQuery(queryFn)로 호출하고 있어, 기본 설정(refetchOnWindowFocus 등)으로 인해 포커스/리마운트 시 동일한 POST가 여러 번 재호출될 수 있습니다. 해당 엔드포인트가 무거운 작업이거나 서버 상태를 만들 경우 문제가 될 수 있으니, (1) 가능하면 GET으로 변경하거나 (2) queryOptions에서 refetch를 끄고 staleTime을 두거나 (3) mutation으로 분리해 명시적으로만 호출하는 쪽을 검토해주세요.
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (19)
src/pages/Club/ClubDetail/components/ClubIntro.tsx (1)
38-44: 타이포그래피 토큰명을 규칙에 맞게 맞춰주세요.Line 42의
text-caption2는 현재 가이드의 토큰 네이밍(text-cap1/text-cap2)과 불일치합니다.🔧 제안 수정
- <div className="text-caption2 text-indigo-300">{item.label}</div> + <div className="text-cap2 text-indigo-300">{item.label}</div>As per coding guidelines, "Use typography tokens (
text-h1totext-h5,text-sub1totext-sub4,text-body1totext-body3,text-cap1totext-cap2) for consistent typography".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Club/ClubDetail/components/ClubIntro.tsx` around lines 38 - 44, In ClubIntro's JSX mapping (clubInfoItems loop inside component ClubIntro) the typography token "text-caption2" is invalid per our guidelines; update the label's className to use the correct token "text-cap2" (or "text-cap1" if that was intended) so it follows the allowed tokens (text-h1..h5, text-sub1..sub4, text-body1..body3, text-cap1..cap2); locate the div with className="text-caption2 text-indigo-300" and replace "text-caption2" with the appropriate "text-capX" token.src/pages/User/MyPage/components/ManagerInfoCard.tsx (1)
42-42: 타이포그래피 토큰을 우선 사용해주세요.여기 텍스트는 커스텀
leading조합보다text-sub*/text-body*토큰을 먼저 쓰는 쪽이 일관성에 더 맞습니다.✏️ 제안 코드
- <div className="leading-[1.6] font-bold text-indigo-700">{currentClub?.name} 정보</div> + <div className="text-sub1 font-bold text-indigo-700">{currentClub?.name} 정보</div>As per coding guidelines,
src/{pages,components,layouts}/**/*.tsx: Use semantic typography utilities (text-h1,text-body1, etc.) before custom CSS.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/User/MyPage/components/ManagerInfoCard.tsx` at line 42, The class on the club title in ManagerInfoCard uses a custom leading utility (leading-[1.6]) instead of the project's semantic typography tokens; update the element rendering {currentClub?.name} so it uses the appropriate semantic token (e.g., text-sub* or text-body* per your design system) combined with font-bold and text-indigo-700, and remove the custom leading-[1.6] to restore consistent typography across src/pages/User/MyPage/components/ManagerInfoCard.tsx.src/pages/Manager/ManagedMemberList/components/MemberCard.tsx (1)
38-41: 텍스트 크기는 semantic typography 토큰으로 통일해 주세요.직접 px 지정 대신
text-sub*/text-body*를 쓰면 페이지 전반 일관성이 좋아집니다.✏️ 제안 코드
- <div className="truncate text-[15px] leading-6 font-semibold text-indigo-700"> + <div className="text-sub1 truncate font-semibold text-indigo-700"> {name} ({studentNumber}) </div> - <div className="text-[13px] leading-[1.6] font-medium text-indigo-300">{positionLabel}</div> + <div className="text-sub3 font-medium text-indigo-300">{positionLabel}</div>As per coding guidelines,
src/{pages,components,layouts}/**/*.tsx: Use semantic typography utilities (text-h1,text-body1, etc.) before custom CSS.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedMemberList/components/MemberCard.tsx` around lines 38 - 41, The MemberCard component uses hardcoded pixel typography classes (text-[15px], text-[13px]) for the name and positionLabel; replace those with the project's semantic typography utilities (e.g., text-body1 / text-sub or the appropriate text-* tokens used across the app) so the rendered elements tied to name/ studentNumber and positionLabel follow the global typography scale and remove the custom px classes; update the divs that render {name} ({studentNumber}) and {positionLabel} to use the semantic tokens instead of text-[15px] and text-[13px].src/pages/Manager/ManagedMemberList/components/ActionPopupMenu.tsx (2)
54-55:label을 key로 사용 시 중복 위험메뉴 아이템의
label이 중복될 경우 React key 충돌이 발생할 수 있습니다. 고유한 ID나 index를 함께 사용하는 것을 권장합니다.♻️ 제안
- <button - key={label} + <button + key={`${label}-${index}`}또는
PopupMenuItem에id필드를 추가하는 방법도 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedMemberList/components/ActionPopupMenu.tsx` around lines 54 - 55, The button in ActionPopupMenu.tsx currently uses label as the React key which can collide if labels duplicate; update the key on the mapped <button> (inside ActionPopupMenu) to use a stable unique identifier (e.g., item.id or a combination like `${item.id}-${label}`) or fallback to the map index if no id exists, or add an id field to the PopupMenuItem type and use that as the key to guarantee uniqueness.
42-42: 오버레이 접근성 개선 권장스크린 리더 사용자를 위해 백그라운드 오버레이에
aria-hidden="true"를 추가하면 좋습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedMemberList/components/ActionPopupMenu.tsx` at line 42, The overlay div in ActionPopupMenu (the element rendered as <div className="fixed inset-0 z-100" onClick={onClose}>) should include aria-hidden="true" to hide the background overlay from screen readers; update that element to add aria-hidden="true" (and optionally tabIndex={-1} if presentational) so the overlay is not announced or focusable while the popup is open.src/pages/Manager/ManagedMemberList/hooks/useRoleManage.ts (1)
97-115: Set에서 값 추출 시 타입 안전성 개선 권장
selectedUserIds.values().next().value를as number | undefined로 캐스팅하고 있습니다. 더 안전한 패턴을 사용하면 좋겠습니다.♻️ 제안
- const nextPresidentId = selectedUserIds.values().next().value as number | undefined; + const [nextPresidentId] = [...selectedUserIds];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedMemberList/hooks/useRoleManage.ts` around lines 97 - 115, In useRoleManage's handleSubmit replace the unsafe cast of selectedUserIds.values().next().value as number | undefined with a proper iterator check: get the iterator result via const iteratorResult = selectedUserIds.values().next(); if iteratorResult.done treat nextPresidentId as undefined else use iteratorResult.value and add a runtime type guard (typeof === 'number') before using it; update the conditional that compares to currentPresident?.userId to use this guarded nextPresidentId and remove the direct "as" cast to ensure type safety.src/pages/Manager/ManagedMemberList/components/MemberSection.tsx (2)
6-6: 타이포그래피 토큰 사용 권장코딩 가이드라인에 따르면
text-sub1~text-sub4등의 타이포그래피 토큰을 우선 사용해야 합니다.text-[14px] leading-[1.6] font-medium대신 적절한 토큰이 있는지 확인해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedMemberList/components/MemberSection.tsx` at line 6, The heading in MemberSection.tsx currently uses explicit utility classes ("text-[14px] leading-[1.6] font-medium") on the <h2> rendering {title}; replace those with the project's typography token (e.g., text-sub2 or the appropriate text-subN token) and remove the manual leading/font-weight utilities; if no existing token matches the intended size/weight, add or update the typography token in the design system and then use that token in the <h2> className to ensure consistency (locate the <h2 className=...> in MemberSection.tsx rendering {title}).
3-9: Props 타입 인라인 정의컴포넌트가 단순하여 인라인 타입도 괜찮지만, 재사용이나 확장 가능성이 있다면 별도 interface 정의를 고려해 보세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedMemberList/components/MemberSection.tsx` around lines 3 - 9, Extract the inline props type into a named interface (e.g., interface MemberSectionProps { title: string; children: ReactNode; }) and update the MemberSection signature to use that interface (function MemberSection(props: MemberSectionProps) or function MemberSection({ title, children }: MemberSectionProps)); keep the prop names title and children as-is and import ReactNode if not already imported.src/index.css (1)
244-251: 애니메이션 유틸리티 클래스 누락
fade-in-up,fade-in-down은 각각.animate-fade-in-up,.animate-fade-in-down클래스가 정의되어 있습니다. 일관성을 위해.animate-ios-spinner-fade클래스도 추가하는 것을 권장합니다 (인라인 스타일로 사용 중이라면 무시해도 됩니다).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/index.css` around lines 244 - 251, The keyframes named ios-spinner-fade are defined but there is no corresponding utility class; add a CSS class named .animate-ios-spinner-fade that applies the ios-spinner-fade keyframes (matching the pattern used by .animate-fade-in-up and .animate-fade-in-down), e.g., setting animation-name: ios-spinner-fade plus the same duration/timing/fill-mode used by the other .animate-* utilities so the spinner animation can be applied via a class rather than inline styles.src/pages/Manager/ManagedMemberList/components/MemberSheetImportModal.tsx (1)
40-50: 접근성:aria-invalid속성 추가 권장에러 상태일 때
aria-invalid={!!errorMessage}를 input에 추가하면 스크린 리더 사용자에게 더 나은 UX를 제공합니다.♻️ 제안
<input type="text" inputMode="url" + aria-invalid={!!errorMessage} autoCapitalize="off"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedMemberList/components/MemberSheetImportModal.tsx` around lines 40 - 50, The input in MemberSheetImportModal is missing an accessibility indicator for error state; update the <input> element in the MemberSheetImportModal component to include aria-invalid={!!errorMessage} (using the existing errorMessage state/prop) so screen readers are informed when the field is invalid, and ensure errorMessage is defined/propagated to that component if not already.src/pages/Manager/ManagedMemberList/components/MemberSheetImportLoadingOverlay.tsx (2)
34-34: 시맨틱 타이포그래피 토큰 사용 권장인라인 스타일(
text-[16px] leading-[1.6]) 대신text-body1등 타이포그래피 유틸리티 사용을 고려하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedMemberList/components/MemberSheetImportLoadingOverlay.tsx` at line 34, The paragraph in MemberSheetImportLoadingOverlay uses hardcoded utility classes ("text-[16px] leading-[1.6]") which should be replaced with the semantic typography token (e.g., "text-body1" or your project's equivalent) to maintain design-system consistency; update the <p> element's className to remove the inline size/line-height utilities and use the semantic token (keeping other classes like "text-text-500 py-1.25 font-semibold") so the component uses the standardized typography utility.
25-25: 하드코딩된 색상값 대신 디자인 토큰 사용 권장
#5a6b7f대신theme.css의 색상 토큰(예:text-*,indigo-*) 사용을 고려하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedMemberList/components/MemberSheetImportLoadingOverlay.tsx` at line 25, The element in MemberSheetImportLoadingOverlay.tsx uses a hardcoded hex color in the className ("bg-[`#5a6b7f`]"); replace that hardcoded value with the appropriate design token from theme.css (for example a Tailwind-style token like bg-indigo-<shade> or a CSS variable token such as bg-[var(--your-token)]) so the component follows the design system; update the className on the same element (the JSX node with className="absolute top-1/2 left-1/2 block h-[28%] w-[8%] rounded-full bg-[`#5a6b7f`]") to use the chosen token and ensure styles remain visually equivalent.src/pages/Manager/ManagedSheetImportPreview/index.tsx (2)
128-131: 하드코딩된 색상 대신 디자인 토큰 사용 권장
#cdcdcd대신border-indigo-*또는border-text-*같은 토큰 사용을 고려하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedSheetImportPreview/index.tsx` around lines 128 - 131, Replace the hardcoded border color in the className expression (the cn(...) call in ManagedSheetImportPreview's JSX) with the appropriate design token utility class (e.g., a border-text-* or border-indigo-* token) instead of 'border-[`#cdcdcd`]'; update the string passed to cn so the element uses the design-system token (keeping the conditional isEnabled text class logic intact) and run a quick style check to ensure the visual matches the intended token.
144-146: 하드코딩된 색상값
#e0e0e0대신 디자인 토큰 사용을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedSheetImportPreview/index.tsx` around lines 144 - 146, The bottom bar div in the ManagedSheetImportPreview component uses a hardcoded color `#e0e0e0` in `border-[`#e0e0e0`]`; replace this with the project's design token (e.g., a CSS variable or tailwind token) so the border color comes from the theme. Update the div in ManagedSheetImportPreview (the element with className containing `border-[`#e0e0e0`]`) to use the appropriate token (for example `border-[var(--color-border)]` or the team’s Tailwind token like `border-line`), and ensure any related styles or tokens are imported/available.src/pages/Manager/ManagedApplicationList/components/ApplicationCard.tsx (1)
52-57: Typography 토큰 사용 권장
text-[15px]와text-[13px]대신 시맨틱 typography 토큰(text-body1,text-sub3등)을 사용하면 일관성이 높아집니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedApplicationList/components/ApplicationCard.tsx` around lines 52 - 57, Replace the hardcoded font-size utility classes in ApplicationCard with the project's semantic typography tokens: change the div with className "truncate text-[15px] leading-6 font-semibold text-indigo-700" to use the appropriate token (e.g., text-body1) while preserving truncate, font-semibold and color, and change the div with "text-[13px] leading-[1.6] font-medium text-indigo-300" to use the smaller semantic token (e.g., text-sub3) while preserving font-medium and the color; update className strings in the ApplicationCard component so they reference the typography tokens instead of literal pixel sizes.src/pages/Manager/ManagedMemberList/hooks/usePreMemberAction.ts (1)
41-49: Mutation 완료 전 UI 상태 정리가 발생함
deletePreMember호출 후 콜백 실행 전에closeDelete()와setSelectedPreMember(null)이 호출됩니다. mutation 실패 시에도 모달이 닫히고 선택이 해제됩니다.
useMemberAction.ts와 동일한 패턴이라 의도적일 수 있지만, mutation 콜백 내에서 UI 정리를 하면 더 안전합니다.♻️ 콜백 내에서 상태 정리 제안
const handleDelete = () => { if (!selectedPreMember) return; deletePreMember(selectedPreMember.preMemberId, { - onSuccess: () => showToast('사전 등록 회원이 삭제되었습니다'), + onSuccess: () => { + showToast('사전 등록 회원이 삭제되었습니다'); + closeDelete(); + setSelectedPreMember(null); + }, onError: (error) => showApiErrorToast(error, '사전 등록 회원 삭제에 실패했습니다.'), + onSettled: () => { + closeDelete(); + setSelectedPreMember(null); + }, }); - closeDelete(); - setSelectedPreMember(null); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedMemberList/hooks/usePreMemberAction.ts` around lines 41 - 49, The handleDelete currently calls closeDelete() and setSelectedPreMember(null) immediately after invoking deletePreMember, which closes the modal and clears selection even if the mutation fails; change this so deletePreMember's mutation callbacks perform UI cleanup: remove the immediate closeDelete() and setSelectedPreMember(null) from handleDelete and instead call them in the onSuccess callback of deletePreMember (optionally leave the modal open or show an error in onError), referencing the handleDelete function and the deletePreMember invocation to locate where to move the cleanup logic.src/pages/Manager/ManagedMemberList/hooks/useMemberAction.ts (1)
50-58: Mutation 완료 전 UI 상태 정리
usePreMemberAction과 동일하게 mutation 콜백 실행 전에 모달이 닫힙니다. 실패 시에도 UI가 정리되는 점 참고 바랍니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Manager/ManagedMemberList/hooks/useMemberAction.ts` around lines 50 - 58, Currently handleRemove calls closeRemove() and setSelectedMember(null) immediately before the removeMember mutation completes; move those UI cleanup calls into the mutation's onSuccess handler so the modal and selection are only cleared after a successful removal. Specifically, in handleRemove (using selectedMember and removeMember), remove the immediate calls to closeRemove and setSelectedMember, and invoke closeRemove() and setSelectedMember(null) inside the onSuccess callback alongside showToast; keep showApiErrorToast in onError so the modal remains open on failure.src/pages/Manager/ManagedMemberList/index.tsx (2)
60-60: 타이포그래피는 semantic token으로 맞춰주세요.Line 60과 Line 70의
text-[15px] leading-6 font-semibold는 직접 값을 박아둔 형태라서, 페이지 레벨에서는 기존text-body*/text-sub*토큰으로 맞추는 편이 일관됩니다.As per coding guidelines, "Use typography tokens (
text-h1totext-h5,text-sub1totext-sub4,text-body1totext-body3,text-cap1totext-cap2) for consistent typography".Also applies to: 65-73
🤖 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 60, Replace hardcoded utility typography classes in the ManagedMemberList component with semantic typography tokens: change occurrences of "text-[15px] leading-6 font-semibold" (e.g., the span showing "총 부원수 : {total}명" and the similar elements in the 65-73 block) to the appropriate token such as "text-body2" or the matching "text-sub*" token per design system (pick the token that maps to 15px/leading-6/semibold), and remove explicit font/leading utilities so all typography uses the project's semantic tokens.
1-12: 상대 경로 import는@/*alias로 통일해주세요.이 파일의
./components/*,./hooks/*는 규칙상 alias import로 맞추는 편이 좋습니다. 바꾸면서 internal import 순서도 함께 정리해주세요.예시 수정
import type { PositionType } from '@/apis/club/entity'; -import ManagerInfoCard from '@/pages/User/MyPage/components/ManagerInfoCard'; -import ActionPopupMenu, { type PopupMenuItem } from './components/ActionPopupMenu'; -import AddMemberModal from './components/AddMemberModal'; -import DeletePreMemberModal from './components/DeletePreMemberModal'; -import MemberCard from './components/MemberCard'; -import MemberSection from './components/MemberSection'; -import MemberSheetImportLoadingOverlay from './components/MemberSheetImportLoadingOverlay'; -import MemberSheetImportModal from './components/MemberSheetImportModal'; -import RemoveMemberModal from './components/RemoveMemberModal'; -import RoleManageModal from './components/RoleManageModal'; -import useManagedMemberList from './hooks/useManagedMemberList'; +import ActionPopupMenu, { type PopupMenuItem } from '@/pages/Manager/ManagedMemberList/components/ActionPopupMenu'; +import AddMemberModal from '@/pages/Manager/ManagedMemberList/components/AddMemberModal'; +import DeletePreMemberModal from '@/pages/Manager/ManagedMemberList/components/DeletePreMemberModal'; +import MemberCard from '@/pages/Manager/ManagedMemberList/components/MemberCard'; +import MemberSection from '@/pages/Manager/ManagedMemberList/components/MemberSection'; +import MemberSheetImportLoadingOverlay from '@/pages/Manager/ManagedMemberList/components/MemberSheetImportLoadingOverlay'; +import MemberSheetImportModal from '@/pages/Manager/ManagedMemberList/components/MemberSheetImportModal'; +import RemoveMemberModal from '@/pages/Manager/ManagedMemberList/components/RemoveMemberModal'; +import RoleManageModal from '@/pages/Manager/ManagedMemberList/components/RoleManageModal'; +import useManagedMemberList from '@/pages/Manager/ManagedMemberList/hooks/useManagedMemberList'; +import ManagerInfoCard from '@/pages/User/MyPage/components/ManagerInfoCard';As per coding guidelines, "Use path alias
@/*for imports instead of relative paths" and "Maintain import order: builtin → external → internal (@/**) → parent → sibling → index, in alphabetical ascending order".🤖 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 1 - 12, Change all relative imports for local modules to use the project alias and reorder imports: replace './components/ManagerInfoCard', './components/ActionPopupMenu', './components/AddMemberModal', './components/DeletePreMemberModal', './components/MemberCard', './components/MemberSection', './components/MemberSheetImportLoadingOverlay', './components/MemberSheetImportModal', './components/RemoveMemberModal', './components/RoleManageModal' and './hooks/useManagedMemberList' with '@/pages/Manager/ManagedMemberList/components/...' and '@/pages/Manager/ManagedMemberList/hooks/useManagedMemberList' (or the correct `@-alias` paths in your project) and then sort imports according to the guideline (builtin → external → internal alias `@/**` → parent → sibling → index) and alphabetically within each group so the internal alias imports (all the components and the hook) are grouped and ordered consistently.
🤖 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/ManagedApplicationList/hooks/useManagedApplicationActions.ts`:
- Around line 42-46: The handleChat handler currently calls createChatRoom
without error handling; wrap the await createChatRoom(userId) call in a
try/catch (mirroring useManagedMemberList.handleCreateChatRoom) so failures are
caught, avoid calling navigate(`/chats/${response.chatRoomId}`) on error, and
surface the error (e.g., show a user-facing toast/notification or log via your
existing logger). Ensure you still call e.stopPropagation() at the top and keep
the successful path unchanged.
In `@src/pages/Manager/ManagedMemberList/components/DeletePreMemberModal.tsx`:
- Line 24: In DeletePreMemberModal.tsx ensure the UI and action are guarded when
memberName is falsy: update the confirmation text (the div showing "정말
{memberName}님의 사전 등록을 삭제하시겠어요?") to render a safe fallback or hide the message
when memberName is undefined, and disable the Delete/confirm button (lines
corresponding to the buttons around 36-37) when memberName is missing; also add
a defensive check inside the confirm handler (e.g., deletePreMember /
onConfirmDelete) to return early if memberName is falsy so the deletion action
cannot proceed.
In `@src/pages/Manager/ManagedMemberList/components/RemoveMemberModal.tsx`:
- Line 33: The confirmation text in the RemoveMemberModal component uses
memberName directly and can render awkwardly or allow deletion when memberName
is empty; update the JSX in RemoveMemberModal (where memberName is referenced in
the paragraph and the subsequent confirmation lines) to use a safe fallback
(e.g., a default label like "회원"/"해당 회원") when memberName is falsy, and also
disable or prevent the delete action (disable the confirm/delete button or guard
the onConfirm handler) when memberName is missing to avoid accidental deletions.
In `@src/pages/Manager/ManagedMemberList/components/RoleManageModal.tsx`:
- Around line 53-66: The member selection button currently only shows a visual
check; add an accessible pressed state by adding aria-pressed={isSelected} to
the button in RoleManageModal.tsx (the button that uses
onMemberClick(member.userId), MemberAvatar, CheckIcon and key={member.userId})
so assistive tech can announce selection; ensure isSelected is a boolean
prop/state and updates when onMemberClick toggles selection so the aria
attribute reflects the current state.
- Around line 5-6: The imports at the top use relative paths; replace them with
project path-alias imports using `@/`* so they follow convention — update the
import statements that bring in MemberAvatar and RoleManageSelector (and the
RoleManageOption type) to use the alias-based module paths (e.g., import
MemberAvatar and import RoleManageSelector/RoleManageOption via `@/`... instead of
'./MemberCard' and './RoleManageSelector') so the codebase consistently uses the
alias convention.
In `@src/pages/Manager/ManagedMemberList/components/RoleManageSelector.tsx`:
- Around line 31-63: The dropdown currently doesn't expose open/selected state
to assistive tech; update the trigger button (in RoleManageSelector) to include
aria-haspopup="menu", aria-expanded tied to isOpen, and aria-controls pointing
to a generated menu id, and add keyboard handling (onKeyDown) to open the menu
with ArrowDown/Enter and close with Escape; mark the menu container with
role="menu" and that id, and each option button with role="menuitem",
tabIndex={-1} (or manage focus) plus aria-selected={selectedOption?.value ===
option.value}; ensure the option click uses onChange(option.value) and close(),
and add focus management so ArrowUp/ArrowDown moves focus between options using
the same menu id and internal index state (use the existing variables/functions
open, close, isOpen, selectedOption, ROLE_OPTIONS, onChange to locate where to
add these attributes and handlers).
In `@src/pages/Manager/ManagedMemberList/hooks/useAddMember.ts`:
- Around line 18-30: handleSubmit currently calls addPreMember(...) then
immediately calls close() and resets state, which closes the modal before the
mutation completes; move the UI-side effects into the mutation callbacks so they
run after the request: remove the synchronous close() and
setStudentNumber/setName calls from handleSubmit and instead call close() and
reset state inside the onSuccess callback of addPreMember (keep onError only
showing the error toast so the modal stays open), and keep the initial guard (if
(!studentNumber || !name) return) intact in handleSubmit; reference handleSubmit
and addPreMember to locate where to apply this change.
In `@src/pages/Manager/ManagedMemberList/hooks/useManagedMemberList.ts`:
- Around line 45-49: handleCreateChatRoom currently awaits createChatRoom
without error handling, which can cause unhandled promise rejections; wrap the
body of handleCreateChatRoom in a try/catch, call
createChatRoom(memberAction.selectedMember.userId) inside the try, verify
chatRoomId before calling navigate(`/chats/${chatRoomId}`), and handle failures
in the catch by logging the error (or calling a notification/toast) and avoiding
navigation; keep checks for memberAction.selectedMember at the top.
In `@src/pages/Manager/ManagedSheetImportPreview/index.tsx`:
- Around line 25-29: The code calls useSuspenseQuery with clubIdNumber =
Number(clubId) without validating clubId, so if Number(clubId) is NaN the
managedClubQueries.sheetImportPreview request may be invalid; update the
component to validate clubId (from useParams) before calling useSuspenseQuery:
parse/convert clubId to a number (e.g., parseInt) and check isNaN, and if
invalid either show a toast via showToast or showApiErrorToast and early-return
a safe fallback (or navigate away) so that managedClubQueries.sheetImportPreview
is only invoked with a valid numeric clubIdNumber.
---
Nitpick comments:
In `@src/index.css`:
- Around line 244-251: The keyframes named ios-spinner-fade are defined but
there is no corresponding utility class; add a CSS class named
.animate-ios-spinner-fade that applies the ios-spinner-fade keyframes (matching
the pattern used by .animate-fade-in-up and .animate-fade-in-down), e.g.,
setting animation-name: ios-spinner-fade plus the same duration/timing/fill-mode
used by the other .animate-* utilities so the spinner animation can be applied
via a class rather than inline styles.
In `@src/pages/Club/ClubDetail/components/ClubIntro.tsx`:
- Around line 38-44: In ClubIntro's JSX mapping (clubInfoItems loop inside
component ClubIntro) the typography token "text-caption2" is invalid per our
guidelines; update the label's className to use the correct token "text-cap2"
(or "text-cap1" if that was intended) so it follows the allowed tokens
(text-h1..h5, text-sub1..sub4, text-body1..body3, text-cap1..cap2); locate the
div with className="text-caption2 text-indigo-300" and replace "text-caption2"
with the appropriate "text-capX" token.
In `@src/pages/Manager/ManagedApplicationList/components/ApplicationCard.tsx`:
- Around line 52-57: Replace the hardcoded font-size utility classes in
ApplicationCard with the project's semantic typography tokens: change the div
with className "truncate text-[15px] leading-6 font-semibold text-indigo-700" to
use the appropriate token (e.g., text-body1) while preserving truncate,
font-semibold and color, and change the div with "text-[13px] leading-[1.6]
font-medium text-indigo-300" to use the smaller semantic token (e.g., text-sub3)
while preserving font-medium and the color; update className strings in the
ApplicationCard component so they reference the typography tokens instead of
literal pixel sizes.
In `@src/pages/Manager/ManagedMemberList/components/ActionPopupMenu.tsx`:
- Around line 54-55: The button in ActionPopupMenu.tsx currently uses label as
the React key which can collide if labels duplicate; update the key on the
mapped <button> (inside ActionPopupMenu) to use a stable unique identifier
(e.g., item.id or a combination like `${item.id}-${label}`) or fallback to the
map index if no id exists, or add an id field to the PopupMenuItem type and use
that as the key to guarantee uniqueness.
- Line 42: The overlay div in ActionPopupMenu (the element rendered as <div
className="fixed inset-0 z-100" onClick={onClose}>) should include
aria-hidden="true" to hide the background overlay from screen readers; update
that element to add aria-hidden="true" (and optionally tabIndex={-1} if
presentational) so the overlay is not announced or focusable while the popup is
open.
In `@src/pages/Manager/ManagedMemberList/components/MemberCard.tsx`:
- Around line 38-41: The MemberCard component uses hardcoded pixel typography
classes (text-[15px], text-[13px]) for the name and positionLabel; replace those
with the project's semantic typography utilities (e.g., text-body1 / text-sub or
the appropriate text-* tokens used across the app) so the rendered elements tied
to name/ studentNumber and positionLabel follow the global typography scale and
remove the custom px classes; update the divs that render {name}
({studentNumber}) and {positionLabel} to use the semantic tokens instead of
text-[15px] and text-[13px].
In `@src/pages/Manager/ManagedMemberList/components/MemberSection.tsx`:
- Line 6: The heading in MemberSection.tsx currently uses explicit utility
classes ("text-[14px] leading-[1.6] font-medium") on the <h2> rendering {title};
replace those with the project's typography token (e.g., text-sub2 or the
appropriate text-subN token) and remove the manual leading/font-weight
utilities; if no existing token matches the intended size/weight, add or update
the typography token in the design system and then use that token in the <h2>
className to ensure consistency (locate the <h2 className=...> in
MemberSection.tsx rendering {title}).
- Around line 3-9: Extract the inline props type into a named interface (e.g.,
interface MemberSectionProps { title: string; children: ReactNode; }) and update
the MemberSection signature to use that interface (function MemberSection(props:
MemberSectionProps) or function MemberSection({ title, children }:
MemberSectionProps)); keep the prop names title and children as-is and import
ReactNode if not already imported.
In
`@src/pages/Manager/ManagedMemberList/components/MemberSheetImportLoadingOverlay.tsx`:
- Line 34: The paragraph in MemberSheetImportLoadingOverlay uses hardcoded
utility classes ("text-[16px] leading-[1.6]") which should be replaced with the
semantic typography token (e.g., "text-body1" or your project's equivalent) to
maintain design-system consistency; update the <p> element's className to remove
the inline size/line-height utilities and use the semantic token (keeping other
classes like "text-text-500 py-1.25 font-semibold") so the component uses the
standardized typography utility.
- Line 25: The element in MemberSheetImportLoadingOverlay.tsx uses a hardcoded
hex color in the className ("bg-[`#5a6b7f`]"); replace that hardcoded value with
the appropriate design token from theme.css (for example a Tailwind-style token
like bg-indigo-<shade> or a CSS variable token such as bg-[var(--your-token)])
so the component follows the design system; update the className on the same
element (the JSX node with className="absolute top-1/2 left-1/2 block h-[28%]
w-[8%] rounded-full bg-[`#5a6b7f`]") to use the chosen token and ensure styles
remain visually equivalent.
In `@src/pages/Manager/ManagedMemberList/components/MemberSheetImportModal.tsx`:
- Around line 40-50: The input in MemberSheetImportModal is missing an
accessibility indicator for error state; update the <input> element in the
MemberSheetImportModal component to include aria-invalid={!!errorMessage} (using
the existing errorMessage state/prop) so screen readers are informed when the
field is invalid, and ensure errorMessage is defined/propagated to that
component if not already.
In `@src/pages/Manager/ManagedMemberList/hooks/useMemberAction.ts`:
- Around line 50-58: Currently handleRemove calls closeRemove() and
setSelectedMember(null) immediately before the removeMember mutation completes;
move those UI cleanup calls into the mutation's onSuccess handler so the modal
and selection are only cleared after a successful removal. Specifically, in
handleRemove (using selectedMember and removeMember), remove the immediate calls
to closeRemove and setSelectedMember, and invoke closeRemove() and
setSelectedMember(null) inside the onSuccess callback alongside showToast; keep
showApiErrorToast in onError so the modal remains open on failure.
In `@src/pages/Manager/ManagedMemberList/hooks/usePreMemberAction.ts`:
- Around line 41-49: The handleDelete currently calls closeDelete() and
setSelectedPreMember(null) immediately after invoking deletePreMember, which
closes the modal and clears selection even if the mutation fails; change this so
deletePreMember's mutation callbacks perform UI cleanup: remove the immediate
closeDelete() and setSelectedPreMember(null) from handleDelete and instead call
them in the onSuccess callback of deletePreMember (optionally leave the modal
open or show an error in onError), referencing the handleDelete function and the
deletePreMember invocation to locate where to move the cleanup logic.
In `@src/pages/Manager/ManagedMemberList/hooks/useRoleManage.ts`:
- Around line 97-115: In useRoleManage's handleSubmit replace the unsafe cast of
selectedUserIds.values().next().value as number | undefined with a proper
iterator check: get the iterator result via const iteratorResult =
selectedUserIds.values().next(); if iteratorResult.done treat nextPresidentId as
undefined else use iteratorResult.value and add a runtime type guard (typeof ===
'number') before using it; update the conditional that compares to
currentPresident?.userId to use this guarded nextPresidentId and remove the
direct "as" cast to ensure type safety.
In `@src/pages/Manager/ManagedMemberList/index.tsx`:
- Line 60: Replace hardcoded utility typography classes in the ManagedMemberList
component with semantic typography tokens: change occurrences of "text-[15px]
leading-6 font-semibold" (e.g., the span showing "총 부원수 : {total}명" and the
similar elements in the 65-73 block) to the appropriate token such as
"text-body2" or the matching "text-sub*" token per design system (pick the token
that maps to 15px/leading-6/semibold), and remove explicit font/leading
utilities so all typography uses the project's semantic tokens.
- Around line 1-12: Change all relative imports for local modules to use the
project alias and reorder imports: replace './components/ManagerInfoCard',
'./components/ActionPopupMenu', './components/AddMemberModal',
'./components/DeletePreMemberModal', './components/MemberCard',
'./components/MemberSection', './components/MemberSheetImportLoadingOverlay',
'./components/MemberSheetImportModal', './components/RemoveMemberModal',
'./components/RoleManageModal' and './hooks/useManagedMemberList' with
'@/pages/Manager/ManagedMemberList/components/...' and
'@/pages/Manager/ManagedMemberList/hooks/useManagedMemberList' (or the correct
`@-alias` paths in your project) and then sort imports according to the guideline
(builtin → external → internal alias `@/**` → parent → sibling → index) and
alphabetically within each group so the internal alias imports (all the
components and the hook) are grouped and ordered consistently.
In `@src/pages/Manager/ManagedSheetImportPreview/index.tsx`:
- Around line 128-131: Replace the hardcoded border color in the className
expression (the cn(...) call in ManagedSheetImportPreview's JSX) with the
appropriate design token utility class (e.g., a border-text-* or border-indigo-*
token) instead of 'border-[`#cdcdcd`]'; update the string passed to cn so the
element uses the design-system token (keeping the conditional isEnabled text
class logic intact) and run a quick style check to ensure the visual matches the
intended token.
- Around line 144-146: The bottom bar div in the ManagedSheetImportPreview
component uses a hardcoded color `#e0e0e0` in `border-[`#e0e0e0`]`; replace this
with the project's design token (e.g., a CSS variable or tailwind token) so the
border color comes from the theme. Update the div in ManagedSheetImportPreview
(the element with className containing `border-[`#e0e0e0`]`) to use the
appropriate token (for example `border-[var(--color-border)]` or the team’s
Tailwind token like `border-line`), and ensure any related styles or tokens are
imported/available.
In `@src/pages/User/MyPage/components/ManagerInfoCard.tsx`:
- Line 42: The class on the club title in ManagerInfoCard uses a custom leading
utility (leading-[1.6]) instead of the project's semantic typography tokens;
update the element rendering {currentClub?.name} so it uses the appropriate
semantic token (e.g., text-sub* or text-body* per your design system) combined
with font-bold and text-indigo-700, and remove the custom leading-[1.6] to
restore consistent typography across
src/pages/User/MyPage/components/ManagerInfoCard.tsx.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 2adc2ebc-646c-43e2-ac83-0f35a9173493
⛔ Files ignored due to path filters (10)
src/assets/image/sheet-guide.pngis excluded by!**/*.png,!src/assets/**and included by**src/assets/svg/assignment-icon.svgis excluded by!**/*.svg,!src/assets/**and included by**src/assets/svg/card-icon.svgis excluded by!**/*.svg,!src/assets/**and included by**src/assets/svg/check-icon.svgis excluded by!**/*.svg,!src/assets/**and included by**src/assets/svg/close-icon.svgis excluded by!**/*.svg,!src/assets/**and included by**src/assets/svg/drop-down-icon.svgis excluded by!**/*.svg,!src/assets/**and included by**src/assets/svg/email-icon.svgis excluded by!**/*.svg,!src/assets/**and included by**src/assets/svg/ink-icon.svgis excluded by!**/*.svg,!src/assets/**and included by**src/assets/svg/person-add-icon.svgis excluded by!**/*.svg,!src/assets/**and included by**src/assets/svg/sms-icon.svgis excluded by!**/*.svg,!src/assets/**and included by**
📒 Files selected for processing (43)
src/App.tsxsrc/apis/club/entity.tssrc/apis/club/index.tssrc/apis/club/managedMutations.tssrc/apis/club/managedQueries.tssrc/components/layout/BottomNav/index.tsxsrc/components/layout/Header/routeTitles.tssrc/index.csssrc/pages/Auth/SignUp/FinishStep.tsxsrc/pages/Chat/ChatRoom.tsxsrc/pages/Club/Application/index.tsxsrc/pages/Club/ClubDetail/components/ClubIntro.tsxsrc/pages/Club/ClubList/components/SearchBar.tsxsrc/pages/Manager/ManagedApplicationList/components/ApplicationCard.tsxsrc/pages/Manager/ManagedApplicationList/hooks/useManagedApplicationActions.tssrc/pages/Manager/ManagedApplicationList/index.tsxsrc/pages/Manager/ManagedClubDetail/index.tsxsrc/pages/Manager/ManagedMemberList/components/ActionPopupMenu.tsxsrc/pages/Manager/ManagedMemberList/components/AddMemberModal.tsxsrc/pages/Manager/ManagedMemberList/components/DeletePreMemberModal.tsxsrc/pages/Manager/ManagedMemberList/components/MemberCard.tsxsrc/pages/Manager/ManagedMemberList/components/MemberSection.tsxsrc/pages/Manager/ManagedMemberList/components/MemberSheetImportLoadingOverlay.tsxsrc/pages/Manager/ManagedMemberList/components/MemberSheetImportModal.tsxsrc/pages/Manager/ManagedMemberList/components/RemoveMemberModal.tsxsrc/pages/Manager/ManagedMemberList/components/RoleManageModal.tsxsrc/pages/Manager/ManagedMemberList/components/RoleManageSelector.tsxsrc/pages/Manager/ManagedMemberList/hooks/useAddMember.tssrc/pages/Manager/ManagedMemberList/hooks/useManagedMemberList.tssrc/pages/Manager/ManagedMemberList/hooks/useMemberAction.tssrc/pages/Manager/ManagedMemberList/hooks/useMemberSheetImport.tssrc/pages/Manager/ManagedMemberList/hooks/usePreMemberAction.tssrc/pages/Manager/ManagedMemberList/hooks/useRoleManage.tssrc/pages/Manager/ManagedMemberList/index.tsxsrc/pages/Manager/ManagedRecruitment/index.tsxsrc/pages/Manager/ManagedRecruitmentForm/index.tsxsrc/pages/Manager/ManagedRecruitmentWrite/index.tsxsrc/pages/Manager/ManagedSheetImportPreview/index.tsxsrc/pages/Manager/components/ApplicationDetailContent.tsxsrc/pages/Manager/hooks/useManagedMemberMutations.tssrc/pages/User/MyPage/components/ManagerInfoCard.tsxsrc/pages/User/MyPage/index.tsxsrc/utils/ts/datetime/date.ts
| const handleChat = async (e: MouseEvent<HTMLButtonElement>, userId: number) => { | ||
| e.stopPropagation(); | ||
| const response = await createChatRoom(userId); | ||
| navigate(`/chats/${response.chatRoomId}`); | ||
| }; |
There was a problem hiding this comment.
handleChat에 에러 처리 누락
useManagedMemberList.handleCreateChatRoom과 동일하게 createChatRoom 실패 시 에러 처리가 없습니다.
🛡️ 에러 처리 추가 제안
const handleChat = async (e: MouseEvent<HTMLButtonElement>, userId: number) => {
e.stopPropagation();
- const response = await createChatRoom(userId);
- navigate(`/chats/${response.chatRoomId}`);
+ try {
+ const response = await createChatRoom(userId);
+ navigate(`/chats/${response.chatRoomId}`);
+ } catch (error) {
+ showApiErrorToast(error, '채팅방 생성에 실패했습니다.');
+ }
};📝 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.
| const handleChat = async (e: MouseEvent<HTMLButtonElement>, userId: number) => { | |
| e.stopPropagation(); | |
| const response = await createChatRoom(userId); | |
| navigate(`/chats/${response.chatRoomId}`); | |
| }; | |
| const handleChat = async (e: MouseEvent<HTMLButtonElement>, userId: number) => { | |
| e.stopPropagation(); | |
| try { | |
| const response = await createChatRoom(userId); | |
| navigate(`/chats/${response.chatRoomId}`); | |
| } catch (error) { | |
| showApiErrorToast(error, '채팅방 생성에 실패했습니다.'); | |
| } | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/pages/Manager/ManagedApplicationList/hooks/useManagedApplicationActions.ts`
around lines 42 - 46, The handleChat handler currently calls createChatRoom
without error handling; wrap the await createChatRoom(userId) call in a
try/catch (mirroring useManagedMemberList.handleCreateChatRoom) so failures are
caught, avoid calling navigate(`/chats/${response.chatRoomId}`) on error, and
surface the error (e.g., show a user-facing toast/notification or log via your
existing logger). Ensure you still call e.stopPropagation() at the top and keep
the successful path unchanged.
| <BottomModal isOpen={isOpen} onClose={onClose}> | ||
| <div className="flex flex-col gap-3 p-5"> | ||
| <div className="text-body2 font-semibold text-indigo-700">사전 등록 삭제</div> | ||
| <div className="text-body3 text-indigo-400">정말 {memberName}님의 사전 등록을 삭제하시겠어요?</div> |
There was a problem hiding this comment.
memberName이 없을 때 확인 문구/삭제 동작을 막아주세요.
현재 memberName이 undefined일 수 있어서 문구가 깨지고, 대상 미확정 상태에서 삭제 버튼이 활성화될 수 있습니다.
🛠️ 제안 코드
export default function DeletePreMemberModal({
@@
}: DeletePreMemberModalProps) {
+ const targetName = memberName ?? '선택한 사용자';
+
return (
@@
- <div className="text-body3 text-indigo-400">정말 {memberName}님의 사전 등록을 삭제하시겠어요?</div>
+ <div className="text-body3 text-indigo-400">정말 {targetName}님의 사전 등록을 삭제하시겠어요?</div>
@@
- disabled={isPending}
+ disabled={isPending || !memberName}Also applies to: 36-37
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/Manager/ManagedMemberList/components/DeletePreMemberModal.tsx` at
line 24, In DeletePreMemberModal.tsx ensure the UI and action are guarded when
memberName is falsy: update the confirmation text (the div showing "정말
{memberName}님의 사전 등록을 삭제하시겠어요?") to render a safe fallback or hide the message
when memberName is undefined, and disable the Delete/confirm button (lines
corresponding to the buttons around 36-37) when memberName is missing; also add
a defensive check inside the confirm handler (e.g., deletePreMember /
onConfirmDelete) to return early if memberName is falsy so the deletion action
cannot proceed.
| </div> | ||
|
|
||
| <div className="px-6.75 pt-5"> | ||
| <p className="text-text-700 text-[15px] leading-[1.6] font-medium">정말 {memberName}님을 삭제하시겠어요?</p> |
There was a problem hiding this comment.
삭제 대상 이름이 없을 때 안전장치가 필요합니다.
memberName이 비어 있으면 확인 문구가 어색해지고, 삭제 버튼도 눌릴 수 있습니다.
🛠️ 제안 코드
export default function RemoveMemberModal({
@@
}: RemoveMemberModalProps) {
+ const targetName = memberName ?? '선택한 사용자';
+
return (
@@
- <p className="text-text-700 text-[15px] leading-[1.6] font-medium">정말 {memberName}님을 삭제하시겠어요?</p>
+ <p className="text-text-700 text-[15px] leading-[1.6] font-medium">정말 {targetName}님을 삭제하시겠어요?</p>
@@
- disabled={isPending}
+ disabled={isPending || !memberName}Also applies to: 47-48
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/Manager/ManagedMemberList/components/RemoveMemberModal.tsx` at line
33, The confirmation text in the RemoveMemberModal component uses memberName
directly and can render awkwardly or allow deletion when memberName is empty;
update the JSX in RemoveMemberModal (where memberName is referenced in the
paragraph and the subsequent confirmation lines) to use a safe fallback (e.g., a
default label like "회원"/"해당 회원") when memberName is falsy, and also disable or
prevent the delete action (disable the confirm/delete button or guard the
onConfirm handler) when memberName is missing to avoid accidental deletions.
| import { MemberAvatar } from './MemberCard'; | ||
| import RoleManageSelector, { type RoleManageOption } from './RoleManageSelector'; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
상대 경로 import를 alias(@/*)로 바꿔주세요.
해당 경로는 프로젝트 컨벤션상 alias import를 사용해야 합니다.
🔧 제안 코드
-import { MemberAvatar } from './MemberCard';
-import RoleManageSelector, { type RoleManageOption } from './RoleManageSelector';
+import { MemberAvatar } from '@/pages/Manager/ManagedMemberList/components/MemberCard';
+import RoleManageSelector, { type RoleManageOption } from '@/pages/Manager/ManagedMemberList/components/RoleManageSelector';As per coding guidelines, src/**/*.{ts,tsx,js,jsx}: Use path alias @/* for imports instead of relative paths.
📝 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.
| import { MemberAvatar } from './MemberCard'; | |
| import RoleManageSelector, { type RoleManageOption } from './RoleManageSelector'; | |
| import { MemberAvatar } from '@/pages/Manager/ManagedMemberList/components/MemberCard'; | |
| import RoleManageSelector, { type RoleManageOption } from '@/pages/Manager/ManagedMemberList/components/RoleManageSelector'; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/Manager/ManagedMemberList/components/RoleManageModal.tsx` around
lines 5 - 6, The imports at the top use relative paths; replace them with
project path-alias imports using `@/`* so they follow convention — update the
import statements that bring in MemberAvatar and RoleManageSelector (and the
RoleManageOption type) to use the alias-based module paths (e.g., import
MemberAvatar and import RoleManageSelector/RoleManageOption via `@/`... instead of
'./MemberCard' and './RoleManageSelector') so the codebase consistently uses the
alias convention.
| <button | ||
| key={member.userId} | ||
| type="button" | ||
| onClick={() => onMemberClick(member.userId)} | ||
| className="flex w-full items-center gap-3 text-left" | ||
| > | ||
| <MemberAvatar name={member.name} /> | ||
| <div className="min-w-0 flex-1"> | ||
| <div className="truncate text-[15px] leading-[1.6] font-semibold text-indigo-700"> | ||
| {member.name} ({member.studentNumber}) | ||
| </div> | ||
| </div> | ||
| {isSelected && <CheckIcon />} | ||
| </button> |
There was a problem hiding this comment.
멤버 선택 버튼에 선택 상태(aria-pressed)를 추가해 주세요.
현재는 체크 아이콘만으로 상태를 표현해서 보조기기 사용자에게 선택 여부 전달이 약합니다.
♿ 제안 코드
<button
key={member.userId}
type="button"
onClick={() => onMemberClick(member.userId)}
+ aria-pressed={isSelected}
+ aria-label={`${member.name} 선택`}
className="flex w-full items-center gap-3 text-left"
>As per coding guidelines, src/pages/**/components/**/*.tsx: 동적 className 조합, 접근성(aria-*, role, 키보드 탐색), Props 타입 일관성을 우선 확인하는지.
📝 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.
| <button | |
| key={member.userId} | |
| type="button" | |
| onClick={() => onMemberClick(member.userId)} | |
| className="flex w-full items-center gap-3 text-left" | |
| > | |
| <MemberAvatar name={member.name} /> | |
| <div className="min-w-0 flex-1"> | |
| <div className="truncate text-[15px] leading-[1.6] font-semibold text-indigo-700"> | |
| {member.name} ({member.studentNumber}) | |
| </div> | |
| </div> | |
| {isSelected && <CheckIcon />} | |
| </button> | |
| <button | |
| key={member.userId} | |
| type="button" | |
| onClick={() => onMemberClick(member.userId)} | |
| aria-pressed={isSelected} | |
| aria-label={`${member.name} 선택`} | |
| className="flex w-full items-center gap-3 text-left" | |
| > | |
| <MemberAvatar name={member.name} /> | |
| <div className="min-w-0 flex-1"> | |
| <div className="truncate text-[15px] leading-[1.6] font-semibold text-indigo-700"> | |
| {member.name} ({member.studentNumber}) | |
| </div> | |
| </div> | |
| {isSelected && <CheckIcon />} | |
| </button> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/Manager/ManagedMemberList/components/RoleManageModal.tsx` around
lines 53 - 66, The member selection button currently only shows a visual check;
add an accessible pressed state by adding aria-pressed={isSelected} to the
button in RoleManageModal.tsx (the button that uses
onMemberClick(member.userId), MemberAvatar, CheckIcon and key={member.userId})
so assistive tech can announce selection; ensure isSelected is a boolean
prop/state and updates when onMemberClick toggles selection so the aria
attribute reflects the current state.
| <button | ||
| type="button" | ||
| onClick={() => (isOpen ? close() : open())} | ||
| className="border-text-300 flex items-center justify-center rounded-full border bg-white pr-1 pl-3" | ||
| > | ||
| <span className="text-sub2 text-text-600">{selectedOption?.label}</span> | ||
| <DropDownIcon /> | ||
| </button> | ||
|
|
||
| {isOpen && ( | ||
| <div className="border-text-300 absolute top-8.5 left-0 z-10 w-18 overflow-hidden rounded-[10px] border bg-white"> | ||
| {ROLE_OPTIONS.map((option, index) => { | ||
| const handleClick = () => { | ||
| onChange(option.value); | ||
| close(); | ||
| }; | ||
|
|
||
| return ( | ||
| <button | ||
| key={option.value} | ||
| type="button" | ||
| onClick={handleClick} | ||
| className={cn( | ||
| 'text-sub2 text-text-600 w-full px-3 py-0.75 text-left', | ||
| index !== ROLE_OPTIONS.length - 1 && 'border-text-200 border-b' | ||
| )} | ||
| > | ||
| {option.label} | ||
| </button> | ||
| ); | ||
| })} | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
드롭다운 상태를 ARIA로 노출해 주세요.
현재 열림/선택 상태가 스크린리더에 충분히 전달되지 않습니다. 트리거와 옵션에 상태 속성을 추가하는 게 좋습니다.
♿ 제안 코드
export default function RoleManageSelector({ onChange, value }: RoleManageSelectorProps) {
@@
+ const listboxId = 'role-manage-selector-options';
@@
<button
type="button"
onClick={() => (isOpen ? close() : open())}
+ aria-haspopup="listbox"
+ aria-expanded={isOpen}
+ aria-controls={listboxId}
className="border-text-300 flex items-center justify-center rounded-full border bg-white pr-1 pl-3"
>
@@
- <div className="border-text-300 absolute top-8.5 left-0 z-10 w-18 overflow-hidden rounded-[10px] border bg-white">
+ <div
+ id={listboxId}
+ role="listbox"
+ className="border-text-300 absolute top-8.5 left-0 z-10 w-18 overflow-hidden rounded-[10px] border bg-white"
+ >
@@
<button
key={option.value}
type="button"
onClick={handleClick}
+ role="option"
+ aria-selected={option.value === value}
className={cn(As per coding guidelines, src/pages/**/components/**/*.tsx: 동적 className 조합, 접근성(aria-*, role, 키보드 탐색), Props 타입 일관성을 우선 확인하는지.
📝 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.
| <button | |
| type="button" | |
| onClick={() => (isOpen ? close() : open())} | |
| className="border-text-300 flex items-center justify-center rounded-full border bg-white pr-1 pl-3" | |
| > | |
| <span className="text-sub2 text-text-600">{selectedOption?.label}</span> | |
| <DropDownIcon /> | |
| </button> | |
| {isOpen && ( | |
| <div className="border-text-300 absolute top-8.5 left-0 z-10 w-18 overflow-hidden rounded-[10px] border bg-white"> | |
| {ROLE_OPTIONS.map((option, index) => { | |
| const handleClick = () => { | |
| onChange(option.value); | |
| close(); | |
| }; | |
| return ( | |
| <button | |
| key={option.value} | |
| type="button" | |
| onClick={handleClick} | |
| className={cn( | |
| 'text-sub2 text-text-600 w-full px-3 py-0.75 text-left', | |
| index !== ROLE_OPTIONS.length - 1 && 'border-text-200 border-b' | |
| )} | |
| > | |
| {option.label} | |
| </button> | |
| ); | |
| })} | |
| </div> | |
| )} | |
| <button | |
| type="button" | |
| onClick={() => (isOpen ? close() : open())} | |
| aria-haspopup="listbox" | |
| aria-expanded={isOpen} | |
| aria-controls="role-manage-selector-options" | |
| className="border-text-300 flex items-center justify-center rounded-full border bg-white pr-1 pl-3" | |
| > | |
| <span className="text-sub2 text-text-600">{selectedOption?.label}</span> | |
| <DropDownIcon /> | |
| </button> | |
| {isOpen && ( | |
| <div | |
| id="role-manage-selector-options" | |
| role="listbox" | |
| className="border-text-300 absolute top-8.5 left-0 z-10 w-18 overflow-hidden rounded-[10px] border bg-white" | |
| > | |
| {ROLE_OPTIONS.map((option, index) => { | |
| const handleClick = () => { | |
| onChange(option.value); | |
| close(); | |
| }; | |
| return ( | |
| <button | |
| key={option.value} | |
| type="button" | |
| onClick={handleClick} | |
| role="option" | |
| aria-selected={option.value === value} | |
| className={cn( | |
| 'text-sub2 text-text-600 w-full px-3 py-0.75 text-left', | |
| index !== ROLE_OPTIONS.length - 1 && 'border-text-200 border-b' | |
| )} | |
| > | |
| {option.label} | |
| </button> | |
| ); | |
| })} | |
| </div> | |
| )} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/Manager/ManagedMemberList/components/RoleManageSelector.tsx` around
lines 31 - 63, The dropdown currently doesn't expose open/selected state to
assistive tech; update the trigger button (in RoleManageSelector) to include
aria-haspopup="menu", aria-expanded tied to isOpen, and aria-controls pointing
to a generated menu id, and add keyboard handling (onKeyDown) to open the menu
with ArrowDown/Enter and close with Escape; mark the menu container with
role="menu" and that id, and each option button with role="menuitem",
tabIndex={-1} (or manage focus) plus aria-selected={selectedOption?.value ===
option.value}; ensure the option click uses onChange(option.value) and close(),
and add focus management so ArrowUp/ArrowDown moves focus between options using
the same menu id and internal index state (use the existing variables/functions
open, close, isOpen, selectedOption, ROLE_OPTIONS, onChange to locate where to
add these attributes and handlers).
| const handleSubmit = () => { | ||
| if (!studentNumber || !name) return; | ||
| addPreMember( | ||
| { studentNumber, name }, | ||
| { | ||
| onSuccess: () => showToast('부원이 추가되었습니다'), | ||
| onError: (error) => showApiErrorToast(error, '부원 추가에 실패했습니다.'), | ||
| } | ||
| ); | ||
| close(); | ||
| setStudentNumber(''); | ||
| setName(''); | ||
| }; |
There was a problem hiding this comment.
close()와 상태 초기화가 mutation 완료 전에 호출됨
mutate() 호출 직후 close()와 상태 초기화가 동기적으로 실행되어, 실제 요청 결과와 무관하게 모달이 닫힙니다. 에러 발생 시에도 모달이 이미 닫혀있어 사용자 경험이 혼란스러울 수 있습니다.
🐛 수정 제안
const handleSubmit = () => {
if (!studentNumber || !name) return;
addPreMember(
{ studentNumber, name },
{
- onSuccess: () => showToast('부원이 추가되었습니다'),
+ onSuccess: () => {
+ showToast('부원이 추가되었습니다');
+ close();
+ setStudentNumber('');
+ setName('');
+ },
onError: (error) => showApiErrorToast(error, '부원 추가에 실패했습니다.'),
}
);
- close();
- setStudentNumber('');
- setName('');
};📝 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.
| const handleSubmit = () => { | |
| if (!studentNumber || !name) return; | |
| addPreMember( | |
| { studentNumber, name }, | |
| { | |
| onSuccess: () => showToast('부원이 추가되었습니다'), | |
| onError: (error) => showApiErrorToast(error, '부원 추가에 실패했습니다.'), | |
| } | |
| ); | |
| close(); | |
| setStudentNumber(''); | |
| setName(''); | |
| }; | |
| const handleSubmit = () => { | |
| if (!studentNumber || !name) return; | |
| addPreMember( | |
| { studentNumber, name }, | |
| { | |
| onSuccess: () => { | |
| showToast('부원이 추가되었습니다'); | |
| close(); | |
| setStudentNumber(''); | |
| setName(''); | |
| }, | |
| onError: (error) => showApiErrorToast(error, '부원 추가에 실패했습니다.'), | |
| } | |
| ); | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/Manager/ManagedMemberList/hooks/useAddMember.ts` around lines 18 -
30, handleSubmit currently calls addPreMember(...) then immediately calls
close() and resets state, which closes the modal before the mutation completes;
move the UI-side effects into the mutation callbacks so they run after the
request: remove the synchronous close() and setStudentNumber/setName calls from
handleSubmit and instead call close() and reset state inside the onSuccess
callback of addPreMember (keep onError only showing the error toast so the modal
stays open), and keep the initial guard (if (!studentNumber || !name) return)
intact in handleSubmit; reference handleSubmit and addPreMember to locate where
to apply this change.
| const handleCreateChatRoom = async () => { | ||
| if (!memberAction.selectedMember) return; | ||
| const { chatRoomId } = await createChatRoom(memberAction.selectedMember.userId); | ||
| navigate(`/chats/${chatRoomId}`); | ||
| }; |
There was a problem hiding this comment.
handleCreateChatRoom에 에러 처리 누락
createChatRoom 호출 실패 시 에러가 처리되지 않아 unhandled promise rejection이 발생할 수 있습니다.
🛡️ 에러 처리 추가 제안
const handleCreateChatRoom = async () => {
if (!memberAction.selectedMember) return;
- const { chatRoomId } = await createChatRoom(memberAction.selectedMember.userId);
- navigate(`/chats/${chatRoomId}`);
+ try {
+ const { chatRoomId } = await createChatRoom(memberAction.selectedMember.userId);
+ navigate(`/chats/${chatRoomId}`);
+ } catch {
+ // useCreateChatRoomMutation에서 에러 처리 또는 toast 표시
+ }
};📝 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.
| const handleCreateChatRoom = async () => { | |
| if (!memberAction.selectedMember) return; | |
| const { chatRoomId } = await createChatRoom(memberAction.selectedMember.userId); | |
| navigate(`/chats/${chatRoomId}`); | |
| }; | |
| const handleCreateChatRoom = async () => { | |
| if (!memberAction.selectedMember) return; | |
| try { | |
| const { chatRoomId } = await createChatRoom(memberAction.selectedMember.userId); | |
| navigate(`/chats/${chatRoomId}`); | |
| } catch { | |
| // useCreateChatRoomMutation에서 에러 처리 또는 toast 표시 | |
| } | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/Manager/ManagedMemberList/hooks/useManagedMemberList.ts` around
lines 45 - 49, handleCreateChatRoom currently awaits createChatRoom without
error handling, which can cause unhandled promise rejections; wrap the body of
handleCreateChatRoom in a try/catch, call
createChatRoom(memberAction.selectedMember.userId) inside the try, verify
chatRoomId before calling navigate(`/chats/${chatRoomId}`), and handle failures
in the catch by logging the error (or calling a notification/toast) and avoiding
navigation; keep checks for memberAction.selectedMember at the top.
| const { clubId } = useParams<{ clubId: string }>(); | ||
| const clubIdNumber = Number(clubId); | ||
| const { showToast } = useToastContext(); | ||
| const showApiErrorToast = useApiErrorToast(); | ||
| const { data: preview } = useSuspenseQuery(managedClubQueries.sheetImportPreview(clubIdNumber)); |
There was a problem hiding this comment.
clubId 유효성 검증 부재
Number(clubId)가 NaN일 경우 useSuspenseQuery가 잘못된 요청을 수행할 수 있습니다. 상위 라우트에서 검증된다면 무시해도 됩니다.
🛡️ 방어적 코드 제안
const clubIdNumber = Number(clubId);
+
+if (Number.isNaN(clubIdNumber)) {
+ throw new Error('Invalid clubId');
+}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/Manager/ManagedSheetImportPreview/index.tsx` around lines 25 - 29,
The code calls useSuspenseQuery with clubIdNumber = Number(clubId) without
validating clubId, so if Number(clubId) is NaN the
managedClubQueries.sheetImportPreview request may be invalid; update the
component to validate clubId (from useParams) before calling useSuspenseQuery:
parse/convert clubId to a number (e.g., parseInt) and check isNaN, and if
invalid either show a toast via showToast or showApiErrorToast and early-return
a safe fallback (or navigate away) so that managedClubQueries.sheetImportPreview
is only invoked with a valid numeric clubIdNumber.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
src/main.tsx (1)
10-10: 새 import는@/별칭으로 맞춰주세요.이번에 추가한 import도 상대 경로보다 alias로 통일하는 편이 규칙과 일관됩니다.
As per coding guidelines, "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/main.tsx` at line 10, Replace the relative import of InAppNotificationToastProvider with the path-alias form: update the import statement for InAppNotificationToastProvider in src/main.tsx to use the "@/contexts/InAppNotificationToastContext" alias instead of "./contexts/InAppNotificationToastContext" so it follows the project rule of using the `@/`* import alias consistently.src/contexts/useInAppNotificationToastContext.tsx (1)
28-38: raw Context export는 피하는 편이 좋습니다.지금 구조는 훅을 우회해서 직접
useContext(InAppNotificationToastContext)를 쓰게 만들 수 있습니다. provider와 hook을 같은 모듈로 묶거나, raw context는 외부에 노출하지 않는 쪽이 안전합니다.
As per coding guidelines, "useContext 래퍼 훅이 제공되어 Context를 직접 노출하지 않는지".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/contexts/useInAppNotificationToastContext.tsx` around lines 28 - 38, The raw context InAppNotificationToastContext is being exported which allows callers to bypass the wrapper hook; remove or make the context module-private and only export the provider and the safe wrapper hook (useInAppNotificationToastContext) so consumers cannot call useContext(InAppNotificationToastContext) directly; update the module to declare InAppNotificationToastContext without export (or rename to _InAppNotificationToastContext) and ensure InAppNotificationToastProvider and useInAppNotificationToastContext remain the publicly exported API.src/components/notification/InAppNotificationToast.tsx (1)
42-55: 토스트 스타일은 토큰/타이포 유틸로 맞춰주세요.
bg-[rgba(...)],text-[16px],text-[#5A6B7F]처럼 하드코딩이 들어오면 같은 UI 계열과 일관성이 깨집니다. 색상은 theme token으로, 텍스트는text-body*/text-sub*계열로 맞추는 쪽이 좋겠습니다.
As per coding guidelines, "Prioritize color tokens fromsrc/styles/theme.css(e.g.,indigo-*,blue-*,background,primary) over hardcoded colors" and "Use typography tokens (text-h1totext-h5,text-sub1totext-sub4,text-body1totext-body3,text-cap1totext-cap2) for consistent typography".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/notification/InAppNotificationToast.tsx` around lines 42 - 55, In InAppNotificationToast (the JSX block rendering the toast, including NotificationToneIcon, the <p> showing {toast.message}, and the action <button> with onClick={onAction}), replace hardcoded color and typography utilities (bg-[rgba(...)] , text-[16px], text-[`#5A6B7F`], etc.) with theme tokens and typography classes: use the appropriate background token (e.g., background/primary/indigo-* per theme), swap the <p> class to a text-body*/text-sub* token (e.g., text-body1 or text-sub1) and remove raw pixel font-size/leading, and change button text color to a theme color token (e.g., indigo-*, blue-*, or text-sub*). Ensure shadow and rounded classes remain but all color and text-size tokens come from the project's theme token set.
🤖 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/notification/InboxNotificationLayer.tsx`:
- Around line 128-145: handleNotificationAction can leave activeToast.onAction()
rejections unhandled; wrap the await activeToast.onAction() call in a try/catch
inside handleNotificationAction (after dismissInAppNotificationToast) and
handle/log the error so it doesn't become an unhandled rejection. Reference the
existing function name handleNotificationAction and the activeToast.onAction
invocation; ensure the onAction prop still calls handleNotificationAction the
same way.
---
Nitpick comments:
In `@src/components/notification/InAppNotificationToast.tsx`:
- Around line 42-55: In InAppNotificationToast (the JSX block rendering the
toast, including NotificationToneIcon, the <p> showing {toast.message}, and the
action <button> with onClick={onAction}), replace hardcoded color and typography
utilities (bg-[rgba(...)] , text-[16px], text-[`#5A6B7F`], etc.) with theme tokens
and typography classes: use the appropriate background token (e.g.,
background/primary/indigo-* per theme), swap the <p> class to a
text-body*/text-sub* token (e.g., text-body1 or text-sub1) and remove raw pixel
font-size/leading, and change button text color to a theme color token (e.g.,
indigo-*, blue-*, or text-sub*). Ensure shadow and rounded classes remain but
all color and text-size tokens come from the project's theme token set.
In `@src/contexts/useInAppNotificationToastContext.tsx`:
- Around line 28-38: The raw context InAppNotificationToastContext is being
exported which allows callers to bypass the wrapper hook; remove or make the
context module-private and only export the provider and the safe wrapper hook
(useInAppNotificationToastContext) so consumers cannot call
useContext(InAppNotificationToastContext) directly; update the module to declare
InAppNotificationToastContext without export (or rename to
_InAppNotificationToastContext) and ensure InAppNotificationToastProvider and
useInAppNotificationToastContext remain the publicly exported API.
In `@src/main.tsx`:
- Line 10: Replace the relative import of InAppNotificationToastProvider with
the path-alias form: update the import statement for
InAppNotificationToastProvider in src/main.tsx to use the
"@/contexts/InAppNotificationToastContext" alias instead of
"./contexts/InAppNotificationToastContext" so it follows the project rule of
using the `@/`* import alias consistently.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 7e247371-a89f-4278-a541-6eb318421301
📒 Files selected for processing (6)
src/components/notification/InAppNotificationToast.tsxsrc/components/notification/InboxNotificationLayer.tsxsrc/contexts/InAppNotificationToastContext.tsxsrc/contexts/useInAppNotificationToastContext.tsxsrc/main.tsxsrc/pages/Manager/ManagedSheetImportPreview/index.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- src/pages/Manager/ManagedSheetImportPreview/index.tsx
| const handleNotificationAction = async () => { | ||
| if (!activeNotification) { | ||
| if (!activeToast) { | ||
| return; | ||
| } | ||
|
|
||
| const targetPath = normalizeInboxNotificationPath(activeNotification.path); | ||
| setNotificationQueue((previousQueue) => previousQueue.slice(1)); | ||
|
|
||
| if (!activeNotification.isRead) { | ||
| try { | ||
| await markAsRead({ notificationId: activeNotification.id }); | ||
| } catch { | ||
| void queryClient.invalidateQueries({ queryKey: notificationQueryKeys.inbox.all() }); | ||
| } | ||
| } | ||
| dismissInAppNotificationToast(activeToast.id); | ||
|
|
||
| if (targetPath) { | ||
| navigate(targetPath); | ||
| if (activeToast.onAction) { | ||
| await activeToast.onAction(); | ||
| } | ||
| }; | ||
|
|
||
| if (isNotificationListPage) { | ||
| return null; | ||
| } | ||
|
|
||
| return <InAppNotificationToast notification={activeNotification} onAction={() => void handleNotificationAction()} />; | ||
| return <InAppNotificationToast toast={activeToast} onAction={() => void handleNotificationAction()} />; | ||
| } |
There was a problem hiding this comment.
onAction 실패가 unhandled rejection으로 남을 수 있습니다.
지금은 Promise를 버리는 형태라 activeToast.onAction()이 reject하면 경계에서 처리되지 않습니다. handleNotificationAction 내부에서 try/catch로 마무리하는 편이 안전합니다.
🔧 예시 수정
const handleNotificationAction = async () => {
if (!activeToast) {
return;
}
dismissInAppNotificationToast(activeToast.id);
if (activeToast.onAction) {
- await activeToast.onAction();
+ try {
+ await activeToast.onAction();
+ } catch {
+ // no-op
+ }
}
};📝 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.
| const handleNotificationAction = async () => { | |
| if (!activeNotification) { | |
| if (!activeToast) { | |
| return; | |
| } | |
| const targetPath = normalizeInboxNotificationPath(activeNotification.path); | |
| setNotificationQueue((previousQueue) => previousQueue.slice(1)); | |
| if (!activeNotification.isRead) { | |
| try { | |
| await markAsRead({ notificationId: activeNotification.id }); | |
| } catch { | |
| void queryClient.invalidateQueries({ queryKey: notificationQueryKeys.inbox.all() }); | |
| } | |
| } | |
| dismissInAppNotificationToast(activeToast.id); | |
| if (targetPath) { | |
| navigate(targetPath); | |
| if (activeToast.onAction) { | |
| await activeToast.onAction(); | |
| } | |
| }; | |
| if (isNotificationListPage) { | |
| return null; | |
| } | |
| return <InAppNotificationToast notification={activeNotification} onAction={() => void handleNotificationAction()} />; | |
| return <InAppNotificationToast toast={activeToast} onAction={() => void handleNotificationAction()} />; | |
| } | |
| const handleNotificationAction = async () => { | |
| if (!activeToast) { | |
| return; | |
| } | |
| dismissInAppNotificationToast(activeToast.id); | |
| if (activeToast.onAction) { | |
| try { | |
| await activeToast.onAction(); | |
| } catch { | |
| // no-op | |
| } | |
| } | |
| }; | |
| if (isNotificationListPage) { | |
| return null; | |
| } | |
| return <InAppNotificationToast toast={activeToast} onAction={() => void handleNotificationAction()} />; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/notification/InboxNotificationLayer.tsx` around lines 128 -
145, handleNotificationAction can leave activeToast.onAction() rejections
unhandled; wrap the await activeToast.onAction() call in a try/catch inside
handleNotificationAction (after dismissInAppNotificationToast) and handle/log
the error so it doesn't become an unhandled rejection. Reference the existing
function name handleNotificationAction and the activeToast.onAction invocation;
ensure the onAction prop still calls handleNotificationAction the same way.
✨ 요약
😎 해결한 이슈
✅ 검증
Summary by CodeRabbit
주요 변경 사항
새로운 기능
스타일
리팩터