Skip to content

[feat] 구글 스프레드 시트 동기화 기능, 관리자 페이지 내 채팅 추가#267

Merged
ff1451 merged 10 commits intodevelopfrom
265-feat-구글-스프레드-시트-동기화-기능-관리자-페이지-내-채팅-추가
Apr 13, 2026

Hidden character warning

The head ref may contain hidden characters: "265-feat-\uad6c\uae00-\uc2a4\ud504\ub808\ub4dc-\uc2dc\ud2b8-\ub3d9\uae30\ud654-\uae30\ub2a5-\uad00\ub9ac\uc790-\ud398\uc774\uc9c0-\ub0b4-\ucc44\ud305-\ucd94\uac00"
Merged

[feat] 구글 스프레드 시트 동기화 기능, 관리자 페이지 내 채팅 추가#267
ff1451 merged 10 commits intodevelopfrom
265-feat-구글-스프레드-시트-동기화-기능-관리자-페이지-내-채팅-추가

Conversation

@ff1451
Copy link
Copy Markdown
Collaborator

@ff1451 ff1451 commented Apr 10, 2026

✨ 요약

- 관리자 페이지 지원자/부원 목록에서 채팅 기능 추가
- 구글 스프레드시트 URL 입력, 미리보기, 최종 등록 흐름 구현
- 관련 관리자 페이지 UI 정리  리팩토링 반영
- 시트 미리보기 하단 액션 바에 리스트가 가려지지 않도록 보정



😎 해결한 이슈



✅ 검증

  • pnpm lint
  • pnpm build

Summary by CodeRabbit

주요 변경 사항

  • 새로운 기능

    • 구글 시트를 이용한 부원 일괄 가져오기(미리보기·확인·진행 상태 표시) 추가
    • 부원 추가/삭제/직책 변경을 위한 모달 및 액션 메뉴 도입
    • 관리자용 신청·채팅·멤버 액션 흐름 개선 컴포넌트 및 훅 추가
    • 앱 내 인앱 알림 토스트 시스템 도입
  • 스타일

    • 네비·헤더·레이아웃 아이콘 및 간격/패딩 조정
    • 로딩 애니메이션 키프레임 추가
  • 리팩터

    • 멤버/신청 관련 로직을 훅·컴포넌트로 분리하여 구조화

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 10, 2026

Walkthrough

Google Sheets 동기화 기능(스프레드시트 URL 입력, 미리보기, 확인)과 관리자용 개별 채팅 흐름을 추가했습니다. 클럽 관련 API 타입/함수(putClubSheet, postClubSheetImportPreview, postClubSheetImportConfirm)와 React Query 키/뮤테이션을 도입했고, ManagedMemberList를 훅/컴포넌트로 대대적 리팩토링했습니다. 여러 모달(시트 임포트, 추가/삭제/역할 변경 등), 액션 팝업, 로딩 오버레이 및 미리보기 페이지가 새로 추가되었고 일부 이벤트 타입과 날짜 포매터 명칭을 정리했습니다.

Possibly related PRs

  • PR 221: managed-club mutation/query 인터페이스 및 훅 변경과 직접적으로 겹쳐 sheet 관련 mutation/키 추가와 연관됨.
  • PR 115: pre-member 타입/API 변경과 pre-member UI(삭제 등) 사용이 코드 수준에서 연결됨.
  • PR 125: 클럽 API 엔티티·엔드포인트 변경과 타입 추가가 중복되거나 상호 의존적임.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 구글 스프레드시트 동기화 기능과 관리자 페이지 채팅 추가라는 주요 변경사항을 명확히 요약하고 있습니다.
Linked Issues check ✅ Passed PR이 이슈 #265의 모든 주요 목표를 충족합니다: 인명부 불러오기 UI/모달, 시트 미리보기 페이지, API 연결, 관리자 페이지 채팅 기능이 구현되었습니다.
Out of Scope Changes check ✅ Passed Tailwind 클래스 리팩토링(h-[46px]→h-11.5), 함수명 변경(formatIsoDateToYYYYMMDD→formatIsoDateToYyyyMmDd), React FormEvent→SubmitEvent 타입 정정 등은 모두 코드 품질 개선으로 스코프 내입니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 265-feat-구글-스프레드-시트-동기화-기능-관리자-페이지-내-채팅-추가

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ff1451 ff1451 marked this pull request as ready for review April 10, 2026 13:03
@ff1451 ff1451 requested a review from Copilot April 10, 2026 13:03
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 41 to +42
<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>
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currentClub가 없는 경우 alt가 "undefined 동아리 사진"처럼 렌더링될 수 있습니다. 접근성 측면에서 의미 있는 기본 alt(예: "동아리 사진")를 사용하거나, club 이름이 없을 때는 alt를 분기 처리하고 이미지가 없으면 placeholder/aria-hidden 처리도 고려해주세요.

Copilot uses AI. Check for mistakes.
/>
))}
</div>
<p className="text-text-500 py-1.25 text-[16px] leading-[1.6] font-semibold">인명부를 불러오는중..</p>
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로딩 문구의 띄어쓰기/말줄임표가 어색합니다. 사용자에게 더 자연스럽게 보이도록 문구를 다듬어 주세요.

Copilot uses AI. Check for mistakes.
Comment on lines +91 to +94
아래 인원이 부원으로 새로 등록될 예정이에요.
<br />
등록하기를 누르면 자동으로 등록 돼요.
</p>
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

문구의 띄어쓰기가 어색합니다. 한국어 맞춤법상 "등록돼요"처럼 붙여 쓰는 편이 자연스럽습니다.

Copilot uses AI. Check for mistakes.
Comment on lines +161 to +165
sheetImportPreview: (clubId: number) =>
queryOptions({
queryKey: managedClubQueryKeys.sheetImportPreview(clubId),
queryFn: () => postClubSheetImportPreview(clubId),
}),
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sheetImportPreviewPOST 요청을 useQuery(queryFn)로 호출하고 있어, 기본 설정(refetchOnWindowFocus 등)으로 인해 포커스/리마운트 시 동일한 POST가 여러 번 재호출될 수 있습니다. 해당 엔드포인트가 무거운 작업이거나 서버 상태를 만들 경우 문제가 될 수 있으니, (1) 가능하면 GET으로 변경하거나 (2) queryOptions에서 refetch를 끄고 staleTime을 두거나 (3) mutation으로 분리해 명시적으로만 호출하는 쪽을 검토해주세요.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-h1 to text-h5, text-sub1 to text-sub4, text-body1 to text-body3, text-cap1 to text-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}`}

또는 PopupMenuItemid 필드를 추가하는 방법도 있습니다.

🤖 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().valueas 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-h1 to text-h5, text-sub1 to text-sub4, text-body1 to text-body3, text-cap1 to text-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

📥 Commits

Reviewing files that changed from the base of the PR and between fef7d69 and 380a13b.

⛔ Files ignored due to path filters (10)
  • src/assets/image/sheet-guide.png is excluded by !**/*.png, !src/assets/** and included by **
  • src/assets/svg/assignment-icon.svg is excluded by !**/*.svg, !src/assets/** and included by **
  • src/assets/svg/card-icon.svg is excluded by !**/*.svg, !src/assets/** and included by **
  • src/assets/svg/check-icon.svg is excluded by !**/*.svg, !src/assets/** and included by **
  • src/assets/svg/close-icon.svg is excluded by !**/*.svg, !src/assets/** and included by **
  • src/assets/svg/drop-down-icon.svg is excluded by !**/*.svg, !src/assets/** and included by **
  • src/assets/svg/email-icon.svg is excluded by !**/*.svg, !src/assets/** and included by **
  • src/assets/svg/ink-icon.svg is excluded by !**/*.svg, !src/assets/** and included by **
  • src/assets/svg/person-add-icon.svg is excluded by !**/*.svg, !src/assets/** and included by **
  • src/assets/svg/sms-icon.svg is excluded by !**/*.svg, !src/assets/** and included by **
📒 Files selected for processing (43)
  • src/App.tsx
  • src/apis/club/entity.ts
  • src/apis/club/index.ts
  • src/apis/club/managedMutations.ts
  • src/apis/club/managedQueries.ts
  • src/components/layout/BottomNav/index.tsx
  • src/components/layout/Header/routeTitles.ts
  • src/index.css
  • src/pages/Auth/SignUp/FinishStep.tsx
  • src/pages/Chat/ChatRoom.tsx
  • src/pages/Club/Application/index.tsx
  • src/pages/Club/ClubDetail/components/ClubIntro.tsx
  • src/pages/Club/ClubList/components/SearchBar.tsx
  • src/pages/Manager/ManagedApplicationList/components/ApplicationCard.tsx
  • src/pages/Manager/ManagedApplicationList/hooks/useManagedApplicationActions.ts
  • src/pages/Manager/ManagedApplicationList/index.tsx
  • src/pages/Manager/ManagedClubDetail/index.tsx
  • src/pages/Manager/ManagedMemberList/components/ActionPopupMenu.tsx
  • src/pages/Manager/ManagedMemberList/components/AddMemberModal.tsx
  • src/pages/Manager/ManagedMemberList/components/DeletePreMemberModal.tsx
  • src/pages/Manager/ManagedMemberList/components/MemberCard.tsx
  • src/pages/Manager/ManagedMemberList/components/MemberSection.tsx
  • src/pages/Manager/ManagedMemberList/components/MemberSheetImportLoadingOverlay.tsx
  • src/pages/Manager/ManagedMemberList/components/MemberSheetImportModal.tsx
  • src/pages/Manager/ManagedMemberList/components/RemoveMemberModal.tsx
  • src/pages/Manager/ManagedMemberList/components/RoleManageModal.tsx
  • src/pages/Manager/ManagedMemberList/components/RoleManageSelector.tsx
  • src/pages/Manager/ManagedMemberList/hooks/useAddMember.ts
  • src/pages/Manager/ManagedMemberList/hooks/useManagedMemberList.ts
  • src/pages/Manager/ManagedMemberList/hooks/useMemberAction.ts
  • src/pages/Manager/ManagedMemberList/hooks/useMemberSheetImport.ts
  • src/pages/Manager/ManagedMemberList/hooks/usePreMemberAction.ts
  • src/pages/Manager/ManagedMemberList/hooks/useRoleManage.ts
  • src/pages/Manager/ManagedMemberList/index.tsx
  • src/pages/Manager/ManagedRecruitment/index.tsx
  • src/pages/Manager/ManagedRecruitmentForm/index.tsx
  • src/pages/Manager/ManagedRecruitmentWrite/index.tsx
  • src/pages/Manager/ManagedSheetImportPreview/index.tsx
  • src/pages/Manager/components/ApplicationDetailContent.tsx
  • src/pages/Manager/hooks/useManagedMemberMutations.ts
  • src/pages/User/MyPage/components/ManagerInfoCard.tsx
  • src/pages/User/MyPage/index.tsx
  • src/utils/ts/datetime/date.ts

Comment on lines +42 to +46
const handleChat = async (e: MouseEvent<HTMLButtonElement>, userId: number) => {
e.stopPropagation();
const response = await createChatRoom(userId);
navigate(`/chats/${response.chatRoomId}`);
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

memberName이 없을 때 확인 문구/삭제 동작을 막아주세요.

현재 memberNameundefined일 수 있어서 문구가 깨지고, 대상 미확정 상태에서 삭제 버튼이 활성화될 수 있습니다.

🛠️ 제안 코드
 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>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

삭제 대상 이름이 없을 때 안전장치가 필요합니다.

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.

Comment on lines +5 to +6
import { MemberAvatar } from './MemberCard';
import RoleManageSelector, { type RoleManageOption } from './RoleManageSelector';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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.

Comment on lines +53 to +66
<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>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

멤버 선택 버튼에 선택 상태(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.

Suggested change
<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.

Comment on lines +31 to +63
<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>
)}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

드롭다운 상태를 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.

Suggested change
<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).

Comment on lines +18 to +30
const handleSubmit = () => {
if (!studentNumber || !name) return;
addPreMember(
{ studentNumber, name },
{
onSuccess: () => showToast('부원이 추가되었습니다'),
onError: (error) => showApiErrorToast(error, '부원 추가에 실패했습니다.'),
}
);
close();
setStudentNumber('');
setName('');
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines +45 to +49
const handleCreateChatRoom = async () => {
if (!memberAction.selectedMember) return;
const { chatRoomId } = await createChatRoom(memberAction.selectedMember.userId);
navigate(`/chats/${chatRoomId}`);
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines +25 to +29
const { clubId } = useParams<{ clubId: string }>();
const clubIdNumber = Number(clubId);
const { showToast } = useToastContext();
const showApiErrorToast = useApiErrorToast();
const { data: preview } = useSuspenseQuery(managedClubQueries.sheetImportPreview(clubIdNumber));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (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 from src/styles/theme.css (e.g., indigo-*, blue-*, background, primary) over hardcoded colors" and "Use typography tokens (text-h1 to text-h5, text-sub1 to text-sub4, text-body1 to text-body3, text-cap1 to text-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

📥 Commits

Reviewing files that changed from the base of the PR and between 380a13b and a6753f3.

📒 Files selected for processing (6)
  • src/components/notification/InAppNotificationToast.tsx
  • src/components/notification/InboxNotificationLayer.tsx
  • src/contexts/InAppNotificationToastContext.tsx
  • src/contexts/useInAppNotificationToastContext.tsx
  • src/main.tsx
  • src/pages/Manager/ManagedSheetImportPreview/index.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/pages/Manager/ManagedSheetImportPreview/index.tsx

Comment on lines 128 to 145
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()} />;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

@ff1451 ff1451 merged commit 206d51b into develop Apr 13, 2026
3 checks passed
@ff1451 ff1451 self-assigned this Apr 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 구글 스프레드 시트 동기화 기능, 관리자 페이지 내 채팅 추가

2 participants