Skip to content

[refactor] 관리자 페이지 및 마이페이지 UI 수정 및 리팩토링#117

Merged
ff1451 merged 21 commits intodevelopfrom
116-refactor-관리자-페이지-및-마이페이지-ui-수정-및-리팩토링
Feb 19, 2026

Hidden character warning

The head ref may contain hidden characters: "116-refactor-\uad00\ub9ac\uc790-\ud398\uc774\uc9c0-\ubc0f-\ub9c8\uc774\ud398\uc774\uc9c0-ui-\uc218\uc815-\ubc0f-\ub9ac\ud329\ud1a0\ub9c1"
Merged

[refactor] 관리자 페이지 및 마이페이지 UI 수정 및 리팩토링#117
ff1451 merged 21 commits intodevelopfrom
116-refactor-관리자-페이지-및-마이페이지-ui-수정-및-리팩토링

Conversation

@ff1451
Copy link
Collaborator

@ff1451 ff1451 commented Feb 17, 2026

Summary by CodeRabbit

  • 새로운 기능

    • 전역 토스트 알림 시스템 도입 및 ToggleSwitch/StatusCard 컴포넌트 추가
    • 클럽 설정(모집/지원/회비) 조회·수정 기능 UI 추가
  • 개선 사항

    • 채팅 자동 스크롤 및 스크롤 동작 개선
    • 매니저용 모집/회비/멤버 관리 UI와 폼 흐름 단순화
    • 회비/계좌 표시 개선: 비어있을 경우 대시로 표시, 납부 기한 노출 제거
    • 내비게이션 뒷단 경로 처리 개선
  • 버그 수정

    • 헤더들의 레이어링(z-index) 충돌 해결

@ff1451 ff1451 self-assigned this Feb 17, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

매니저 훅 모듈을 도메인별로 분리하고 기존의 useManagerQuery 파일을 삭제함. 매니저용 훅 세트(useManagedClubs, useManagedFee, useManagedMembers, useManagedApplications, useManagedRecruitment, useManagedSettings 등)를 추가함. 클럽 API 타입·엔드포인트가 변경되어 ClubFeeRequest/ClubFeeResponse, ClubSettingsResponse/ClubSettingsPatchRequest 등이 추가·수정됨. 전역 Toast 컨텍스트(ToastProvider, useToastContext), 채팅 스크롤 훅(useChatRoomScroll), UI 컴포넌트 ToggleSwitch·StatusCard, 다수 헤더의 z-index 조정 등이 포함됨.

Possibly related PRs

  • PR 104: 회비 관련 타입·엔드포인트 변경사항(예: isFeeRequired, ClubFeeResponse/ClubFeeRequest)을 다루어 API 및 클라이언트 변경과 직접적인 코드 수준 연결이 있음.
  • PR 117: 동일 파일들(src/apis/club/entity.ts, index.ts)과 매니저 훅·컴포넌트 리팩터에 중복된 코드 변경이 있어 강한 코드 수준 연관성이 있음.
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 변경 사항의 핵심을 명확하게 반영하고 있습니다. 관리자 페이지 및 마이페이지의 UI 수정 및 리팩토링이라는 주요 목표가 잘 드러나 있습니다.

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

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 116-refactor-관리자-페이지-및-마이페이지-ui-수정-및-리팩토링

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

Copy link

@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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/pages/Club/Application/components/AccountInfo.tsx (1)

35-47: ⚠️ Potential issue | 🟡 Minor

계좌번호 없을 때 복사 버튼 동작 검토

accountNumbernull일 때 '-'로 표시되지만, 복사 시 빈 문자열이 복사됩니다. 사용자 혼란을 줄 수 있습니다.

💡 계좌번호 없을 때 버튼 비활성화 제안
       <button
+        disabled={!accountInfo.accountNumber}
         onClick={async () => {
+          if (!accountInfo.accountNumber) return;
           try {
-            await navigator.clipboard.writeText(accountInfo.accountNumber ?? '');
+            await navigator.clipboard.writeText(accountInfo.accountNumber);
             showToast('계좌번호가 복사되었습니다');
           } catch {
             showToast('복사에 실패했습니다');
           }
         }}
-        className="bg-primary text-indigo-0 flex items-center justify-center gap-1.5 rounded-sm py-3 text-xs font-medium"
+        className="bg-primary text-indigo-0 flex items-center justify-center gap-1.5 rounded-sm py-3 text-xs font-medium disabled:cursor-not-allowed disabled:opacity-50"
       >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Club/Application/components/AccountInfo.tsx` around lines 35 - 47,
The copy button currently writes an empty string when accountInfo.accountNumber
is null which is confusing; update the button (the onClick handler attached to
the Copy button and the accountInfo.accountNumber check) to be disabled when
accountInfo.accountNumber is null/empty, prevent clipboard writes in that case,
and show a clear toast like "계좌번호가 없습니다" instead. Specifically: use a boolean
like const hasAccount = !!accountInfo.accountNumber to set disabled={
!hasAccount } and add aria-disabled, change the onClick to early-return if
!hasAccount (or only call
navigator.clipboard.writeText(accountInfo.accountNumber) when hasAccount), and
ensure the button styling toggles to a disabled style while keeping showToast
for success and the new failure message for missing account.
🧹 Nitpick comments (10)
src/pages/Chat/ChatRoom.tsx (1)

49-66: scrollToBottom() 호출 타이밍 확인 필요.

sendMessage는 비동기로 동작하므로 Line 65의 scrollToBottom() 호출 시점에는 새 메시지가 아직 렌더링되지 않았을 수 있습니다. 다만, useChatRoomScrolluseLayoutEffectchatMessagesLength 변경 시 자동으로 스크롤하고, scrollToBottom()isNearBottomRef.current = true를 설정하므로 의도된 동작이라면 문제없습니다.

명시적으로 의도를 표현하려면 주석을 추가하는 것을 권장합니다.

📝 의도 명확화를 위한 주석 추가 제안
     setValue('');
     if (textareaRef.current) {
       textareaRef.current.style.height = 'auto';
       textareaRef.current.focus();
     }
+    // isNearBottomRef를 true로 설정하여 메시지 수신 시 자동 스크롤 보장
     scrollToBottom();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Chat/ChatRoom.tsx` around lines 49 - 66, The immediate call to
scrollToBottom() in handleSubmit may run before the new message is rendered
because sendMessage is async; clarify intent by adding a concise comment in
handleSubmit near the sendMessage and scrollToBottom calls explaining that
scrollToBottom() is intentionally invoked immediately to set
isNearBottomRef.current = true (so the UI treats the user as near-bottom), while
the actual scroll to the new message is handled by useChatRoomScroll's
useLayoutEffect when chatMessagesLength updates; reference handleSubmit,
sendMessage, scrollToBottom, useChatRoomScroll, isNearBottomRef,
useLayoutEffect, chatMessagesLength and textareaRef in the comment so future
readers understand the ordering and why this call is correct.
src/pages/Chat/hooks/useChatRoomScroll.ts (1)

63-77: scrollContainerRef.current 변경 감지 누락 가능성.

chatRoomId가 변경되지 않고 scrollContainerRef.current만 변경되는 경우, 이벤트 리스너가 재등록되지 않을 수 있습니다. 현재 구조에서는 문제가 없지만, ref 대상이 동적으로 변경될 가능성이 있다면 검토가 필요합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Chat/hooks/useChatRoomScroll.ts` around lines 63 - 77, The effect
that attaches the scroll listener to scrollContainerRef.current can miss when
the ref target changes because only chatRoomId is in the dependency array;
update the logic in useChatRoomScroll: capture the current node (const container
= scrollContainerRef.current) and include that container node in the effect
dependencies (or replace the ref with a callback ref/state like containerNode
via setContainerNode) so that when scrollContainerRef.current changes the effect
re-runs, and ensure handleScroll (which updates isNearBottomRef using
isNearBottom) is removed from the previous container and added to the new one
during cleanup/attach.
src/pages/Manager/ManagedMemberList/index.tsx (1)

511-511: Toast 렌더링은 Provider에 맡기는 편이 낫습니다.
ToastProvider가 이미 전역 Toast를 렌더링하므로 toast={null} + no-op은 혼란만 줄 수 있습니다.

🧹 제안 변경
-      <Toast toast={null} onClose={() => {}} />
🤖 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 511, Remove the
explicit Toast usage in ManagedMemberList (the line rendering <Toast
toast={null} onClose={() => {}} />) and rely on the existing ToastProvider to
render global toasts; specifically, delete that component call from the
ManagedMemberList render/return so you don't pass a null toast or a no-op
onClose handler, ensuring ToastProvider handles toast lifecycle and presentation
instead.
src/apis/club/index.ts (1)

70-73: 백엔드 API 스펙 문서화 권장합니다.

putClubFee의 응답이 ClubFeeResponse로 타입되었으나, 실제 백엔드 응답 스펙(응답 바디 포함 여부, 필드 구조)을 API 문서에 명시하면 좋습니다. 현재 프론트엔드는 모든 필드를 nullable로 정의(amount: number | null 등)하고 UI에서 ?? 연산자로 null을 처리하므로 런타임 에러는 발생하지 않으나, 명시적인 API 계약 정의로 예상치 못한 변경을 방지할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/apis/club/index.ts` around lines 70 - 73, Document the backend API
contract for club fee endpoints and reconcile TypeScript types: confirm and
record the exact response shape (whether body is returned, field names and
nullability) for putClubFee and getClubFee in the API spec, then update the
ClubFeeResponse interface and any calling code (getClubFee, putClubFee) to match
the documented contract (e.g., make fields non-nullable if backend guarantees
them or keep nullable and add explicit runtime guards if not), and add a short
comment referencing the API doc/endpoint so future reviewers can verify the
contract.
src/apis/club/entity.ts (1)

222-238: Union 타입 설계 검토

ClubFeeRequest union 타입은 upsert와 delete 케이스를 잘 구분하지만, discriminator 필드가 없어 런타임에서 구분이 어려울 수 있습니다.

타입 가드 함수나 discriminator 필드 추가를 고려해볼 수 있습니다:

// 예시: 타입 가드
function isDeleteRequest(req: ClubFeeRequest): req is ClubFeeDeleteRequest {
  return req.amount === null;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/apis/club/entity.ts` around lines 222 - 238, The union ClubFeeRequest
(ClubFeeUpsertRequest | ClubFeeDeleteRequest) lacks a runtime discriminator,
making it hard to distinguish cases; add a clear discriminator field (e.g.,
action: 'upsert' | 'delete') to both ClubFeeUpsertRequest and
ClubFeeDeleteRequest or implement and export a type guard function (e.g.,
isDeleteRequest(req: ClubFeeRequest): req is ClubFeeDeleteRequest that checks a
stable property such as amount === null) and update all call sites that branch
on ClubFeeRequest (constructors, handlers, validators) to use the new
discriminator or the type guard (references: ClubFeeRequest,
ClubFeeUpsertRequest, ClubFeeDeleteRequest, isDeleteRequest).
src/pages/Manager/ManagedRecruitment/index.tsx (2)

9-9: import 경로 일관성 유지 필요

다른 import들은 @/ alias를 사용하는데, 이 라인만 상대 경로를 사용하고 있습니다.

♻️ alias 경로로 수정
-import ToggleSwitch from '../../../components/common/ToggleSwitch';
+import ToggleSwitch from '@/components/common/ToggleSwitch';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Manager/ManagedRecruitment/index.tsx` at line 9, The import of
ToggleSwitch in ManagedRecruitment index.tsx uses a relative path; update that
single import (ToggleSwitch) to use the project's alias import style consistent
with the other imports (use the "@/components/common/ToggleSwitch" alias) so all
imports in this file follow the same `@/` alias convention.

13-25: Mock 데이터 및 TODO 확인

TODO 주석이 있어 API 연결 예정임을 알 수 있습니다. 현재 토글 상태가 로컬에만 유지되어 새로고침 시 초기화됩니다.

API 연결을 위한 커스텀 훅 구조가 필요하시면 도움드릴 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Manager/ManagedRecruitment/index.tsx` around lines 13 - 25, The
component is using local mock state (recruitmentEnabled, applicationEnabled,
feeEnabled and derived recruitmentContent, applicationContent, feeContent) with
a TODO to connect to the API; replace this by moving the state and data
fetching/updating into a custom hook (e.g., useManagedRecruitment) that calls
the API to load initial values, exposes setters (setRecruitmentEnabled,
setApplicationEnabled, setFeeEnabled) and derived content, and handles
optimistic updates/error handling; remove the hardcoded mock strings and TODO,
wire the component to useManagedRecruitment to persist state across refreshes
and perform real API updates for the
recruitmentEnabled/applicationEnabled/feeEnabled values.
src/pages/Manager/hooks/useManagedClubs.ts (1)

33-46: mutation onError 핸들러 추가 고려

useUpdateClubInfo에서 성공 시 토스트는 있지만, 실패 시 에러 처리가 누락되었습니다. 호출부에서 처리하고 있다면 문제없지만, 일관성을 위해 여기서도 처리하면 좋습니다.

💡 onError 핸들러 추가 예시
   return useMutation({
     mutationKey: managerClubQueryKeys.managedClubInfo(clubId),
     mutationFn: (data: ClubInfoRequest) => putClubInfo(clubId, data),
     onSuccess: () => {
       showToast('클럽 정보가 수정되었습니다');
       queryClient.invalidateQueries({ queryKey: clubQueryKeys.detail(clubId) });
       navigate(-1);
     },
+    onError: () => {
+      showToast('클럽 정보 수정에 실패했습니다', 'error');
+    },
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Manager/hooks/useManagedClubs.ts` around lines 33 - 46, Add an
onError handler to the useUpdateClubInfo mutation so failures are handled
consistently: update useUpdateClubInfo (the useMutation call keyed by
managerClubQueryKeys.managedClubInfo) to include onError that accepts the error
and optionally the variables, calls showToast with an error message (include
error.message or a user-friendly fallback), and avoids the onSuccess navigation;
optionally also log the error and keep queryClient.invalidateQueries only on
success. This ensures putClubInfo failures surface a toast and are not silently
ignored.
src/pages/Manager/hooks/useManagedRecruitment.ts (1)

21-46: create/update 훅의 navigate(-1) 옵션화 제안.
훅 내부에서 항상 navigate(-1)을 실행하면 호출부에서 다른 경로로 이동시키려 할 때 이중 네비게이션/히스토리 꼬임이 생길 수 있습니다. navigateBack 옵션 추가 또는 네비게이션을 호출부 책임으로 분리하는 편이 안전합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Manager/hooks/useManagedRecruitment.ts` around lines 21 - 46, Both
useCreateRecruitment and useUpdateRecruitment call navigate(-1) unconditionally
which can cause double navigation or history issues; change the hooks to accept
an optional option (e.g., navigateBack?: boolean or onSuccessNavigate?:
(navigate: NavigateFunction) => void) or remove navigation from the hooks and
call navigation in the caller. Specifically update export const
useCreateRecruitment and export const useUpdateRecruitment to accept an options
param, use that option in the onSuccess handler instead of always calling
navigate(-1), and keep showToast behavior unchanged (refer to
useCreateRecruitment, useUpdateRecruitment, navigate(-1), showToast, and
mutationFn/ onSuccess to locate the code).
src/pages/User/MyPage/components/UserInfoCard.tsx (1)

47-71: 관리자 클럽 조회 훅 호출 범위 줄이기.
type === 'user'에서도 useGetManagedClubs가 실행되고, detail 분기에서는 ManagerDetailInfoCard에서도 다시 호출됩니다. 타입별 컴포넌트 분리/훅 이동으로 불필요한 요청과 권한 이슈 가능성을 줄여주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/User/MyPage/components/UserInfoCard.tsx` around lines 47 - 71, The
top-level call to useGetManagedClubs inside UserInfoCard runs for all types
(including 'user' and 'detail') causing unnecessary requests and potential
permission issues; remove the unconditional const { managedClubList } =
useGetManagedClubs() from UserInfoCard and instead call useGetManagedClubs only
in the specific component that needs it (e.g., inside ManagerDetailInfoCard or
in the branch rendered for manager views), or fetch it in a parent and pass
managedClubList as a prop to ManagerDetailInfoCard; update references to
managedClubList accordingly and ensure any navigation handlers (handleCardClick,
handleButtonClick) remain in UserInfoCard while the hook is localized to the
component that requires the managed club data.
🤖 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/contexts/ToastContext.tsx`:
- Around line 14-17: The showToast implementation sets a timeout without
tracking or clearing it, which can cause stale state updates or memory leaks
when showToast is called repeatedly or the component unmounts; modify the
ToastContext to store the timeout id in a ref (e.g., timerRef) inside the
component that defines showToast and, inside showToast, clear any existing
timeoutRef.current before assigning a new setTimeout and saving its id; also add
a useEffect cleanup that clears timerRef.current on unmount; update type
annotations for the ref (number | null or ReturnType<typeof
setTimeout>/NodeJS.Timeout as appropriate) so TypeScript stays happy.

In `@src/main.tsx`:
- Around line 6-7: The lint failure is due to import ordering: move the
ToastProvider import above the installViewportVars import in src/main.tsx;
specifically, swap the two import lines so that the import of ToastProvider
(from './contexts/ToastContext') appears before the import of
installViewportVars (from './utils/ts/viewport.ts'), ensuring the symbols
ToastProvider and installViewportVars remain unchanged and all usages still
work.

In `@src/pages/Manager/ManagedApplictaionDetail/index.tsx`:
- Line 189: Remove the unnecessary hard-coded Toast instance: delete the JSX
<Toast toast={null} onClose={() => {}} /> from the component; if you intend to
show toasts later, wire the component up to the global toast state via
useToastContext and render <Toast toast={toast} onClose={removeToast} /> (or
similar handlers) where toast/removeToast come from the hook, otherwise simply
remove the Toast import/usage to avoid a no-op render.

In `@src/pages/User/MyPage/components/UserInfoCard.tsx`:
- Around line 27-40: The Card currently renders as a div with onClick
(component: Card, handler: handleClick) so it is not keyboard-accessible; either
replace Card with the navigation-aware component NavigateCard to get proper
keyboard handlers, or keep Card and add accessibility: set role="button",
tabIndex={0}, and implement key event handling (onKeyDown) to call handleClick
on Enter and Space, and ensure ARIA labels are present to describe the action.

---

Outside diff comments:
In `@src/pages/Club/Application/components/AccountInfo.tsx`:
- Around line 35-47: The copy button currently writes an empty string when
accountInfo.accountNumber is null which is confusing; update the button (the
onClick handler attached to the Copy button and the accountInfo.accountNumber
check) to be disabled when accountInfo.accountNumber is null/empty, prevent
clipboard writes in that case, and show a clear toast like "계좌번호가 없습니다" instead.
Specifically: use a boolean like const hasAccount = !!accountInfo.accountNumber
to set disabled={ !hasAccount } and add aria-disabled, change the onClick to
early-return if !hasAccount (or only call
navigator.clipboard.writeText(accountInfo.accountNumber) when hasAccount), and
ensure the button styling toggles to a disabled style while keeping showToast
for success and the new failure message for missing account.

---

Nitpick comments:
In `@src/apis/club/entity.ts`:
- Around line 222-238: The union ClubFeeRequest (ClubFeeUpsertRequest |
ClubFeeDeleteRequest) lacks a runtime discriminator, making it hard to
distinguish cases; add a clear discriminator field (e.g., action: 'upsert' |
'delete') to both ClubFeeUpsertRequest and ClubFeeDeleteRequest or implement and
export a type guard function (e.g., isDeleteRequest(req: ClubFeeRequest): req is
ClubFeeDeleteRequest that checks a stable property such as amount === null) and
update all call sites that branch on ClubFeeRequest (constructors, handlers,
validators) to use the new discriminator or the type guard (references:
ClubFeeRequest, ClubFeeUpsertRequest, ClubFeeDeleteRequest, isDeleteRequest).

In `@src/apis/club/index.ts`:
- Around line 70-73: Document the backend API contract for club fee endpoints
and reconcile TypeScript types: confirm and record the exact response shape
(whether body is returned, field names and nullability) for putClubFee and
getClubFee in the API spec, then update the ClubFeeResponse interface and any
calling code (getClubFee, putClubFee) to match the documented contract (e.g.,
make fields non-nullable if backend guarantees them or keep nullable and add
explicit runtime guards if not), and add a short comment referencing the API
doc/endpoint so future reviewers can verify the contract.

In `@src/pages/Chat/ChatRoom.tsx`:
- Around line 49-66: The immediate call to scrollToBottom() in handleSubmit may
run before the new message is rendered because sendMessage is async; clarify
intent by adding a concise comment in handleSubmit near the sendMessage and
scrollToBottom calls explaining that scrollToBottom() is intentionally invoked
immediately to set isNearBottomRef.current = true (so the UI treats the user as
near-bottom), while the actual scroll to the new message is handled by
useChatRoomScroll's useLayoutEffect when chatMessagesLength updates; reference
handleSubmit, sendMessage, scrollToBottom, useChatRoomScroll, isNearBottomRef,
useLayoutEffect, chatMessagesLength and textareaRef in the comment so future
readers understand the ordering and why this call is correct.

In `@src/pages/Chat/hooks/useChatRoomScroll.ts`:
- Around line 63-77: The effect that attaches the scroll listener to
scrollContainerRef.current can miss when the ref target changes because only
chatRoomId is in the dependency array; update the logic in useChatRoomScroll:
capture the current node (const container = scrollContainerRef.current) and
include that container node in the effect dependencies (or replace the ref with
a callback ref/state like containerNode via setContainerNode) so that when
scrollContainerRef.current changes the effect re-runs, and ensure handleScroll
(which updates isNearBottomRef using isNearBottom) is removed from the previous
container and added to the new one during cleanup/attach.

In `@src/pages/Manager/hooks/useManagedClubs.ts`:
- Around line 33-46: Add an onError handler to the useUpdateClubInfo mutation so
failures are handled consistently: update useUpdateClubInfo (the useMutation
call keyed by managerClubQueryKeys.managedClubInfo) to include onError that
accepts the error and optionally the variables, calls showToast with an error
message (include error.message or a user-friendly fallback), and avoids the
onSuccess navigation; optionally also log the error and keep
queryClient.invalidateQueries only on success. This ensures putClubInfo failures
surface a toast and are not silently ignored.

In `@src/pages/Manager/hooks/useManagedRecruitment.ts`:
- Around line 21-46: Both useCreateRecruitment and useUpdateRecruitment call
navigate(-1) unconditionally which can cause double navigation or history
issues; change the hooks to accept an optional option (e.g., navigateBack?:
boolean or onSuccessNavigate?: (navigate: NavigateFunction) => void) or remove
navigation from the hooks and call navigation in the caller. Specifically update
export const useCreateRecruitment and export const useUpdateRecruitment to
accept an options param, use that option in the onSuccess handler instead of
always calling navigate(-1), and keep showToast behavior unchanged (refer to
useCreateRecruitment, useUpdateRecruitment, navigate(-1), showToast, and
mutationFn/ onSuccess to locate the code).

In `@src/pages/Manager/ManagedMemberList/index.tsx`:
- Line 511: Remove the explicit Toast usage in ManagedMemberList (the line
rendering <Toast toast={null} onClose={() => {}} />) and rely on the existing
ToastProvider to render global toasts; specifically, delete that component call
from the ManagedMemberList render/return so you don't pass a null toast or a
no-op onClose handler, ensuring ToastProvider handles toast lifecycle and
presentation instead.

In `@src/pages/Manager/ManagedRecruitment/index.tsx`:
- Line 9: The import of ToggleSwitch in ManagedRecruitment index.tsx uses a
relative path; update that single import (ToggleSwitch) to use the project's
alias import style consistent with the other imports (use the
"@/components/common/ToggleSwitch" alias) so all imports in this file follow the
same `@/` alias convention.
- Around line 13-25: The component is using local mock state
(recruitmentEnabled, applicationEnabled, feeEnabled and derived
recruitmentContent, applicationContent, feeContent) with a TODO to connect to
the API; replace this by moving the state and data fetching/updating into a
custom hook (e.g., useManagedRecruitment) that calls the API to load initial
values, exposes setters (setRecruitmentEnabled, setApplicationEnabled,
setFeeEnabled) and derived content, and handles optimistic updates/error
handling; remove the hardcoded mock strings and TODO, wire the component to
useManagedRecruitment to persist state across refreshes and perform real API
updates for the recruitmentEnabled/applicationEnabled/feeEnabled values.

In `@src/pages/User/MyPage/components/UserInfoCard.tsx`:
- Around line 47-71: The top-level call to useGetManagedClubs inside
UserInfoCard runs for all types (including 'user' and 'detail') causing
unnecessary requests and potential permission issues; remove the unconditional
const { managedClubList } = useGetManagedClubs() from UserInfoCard and instead
call useGetManagedClubs only in the specific component that needs it (e.g.,
inside ManagerDetailInfoCard or in the branch rendered for manager views), or
fetch it in a parent and pass managedClubList as a prop to
ManagerDetailInfoCard; update references to managedClubList accordingly and
ensure any navigation handlers (handleCardClick, handleButtonClick) remain in
UserInfoCard while the hook is localized to the component that requires the
managed club data.

Comment on lines +27 to 40
<Card onClick={handleClick} className="active:bg-indigo-5/50 cursor-pointer">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3">
<img className="h-12 w-12 rounded-full" src={managedClub?.imageUrl} alt="Member Avatar" />
<div>
<div className={twMerge('text-h2 font-bold text-indigo-700')}>{currentClub?.name} 관리자</div>
<div className="mt-1.5 text-xs leading-3.5 font-medium text-indigo-300">
{myInfo.studentNumber} · {myInfo.universityName} · {currentClub?.position}
</div>
</div>
</div>
<RightArrowIcon className="mr-2" />
</div>
</Card>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -type f -name '*Card*' src/components/ -o fd -type f -name '*Card*' src/

Repository: BCSDLab/KONECT_FRONT_END

Length of output: 238


🏁 Script executed:

cat -n src/pages/User/MyPage/components/UserInfoCard.tsx | head -100

Repository: BCSDLab/KONECT_FRONT_END

Length of output: 4310


🏁 Script executed:

find src -name '*Card*' -type f | grep -i card

Repository: BCSDLab/KONECT_FRONT_END

Length of output: 443


🏁 Script executed:

cat -n src/components/common/Card.tsx

Repository: BCSDLab/KONECT_FRONT_END

Length of output: 698


🏁 Script executed:

cat -n src/components/common/NavigateCard.tsx

Repository: BCSDLab/KONECT_FRONT_END

Length of output: 1130


Card 컴포넌트가 div로 렌더링되어 키보드 접근성 미확보.
Card는 plain <div>이므로 onClick 만으로는 Enter/Space 키 조작이 불가능합니다. 네비게이션 의도라면 NavigateCard 컴포넌트를 사용하거나, Card를 유지하려면 role="button", tabIndex={0}, 키 이벤트 핸들링을 추가하세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/User/MyPage/components/UserInfoCard.tsx` around lines 27 - 40, The
Card currently renders as a div with onClick (component: Card, handler:
handleClick) so it is not keyboard-accessible; either replace Card with the
navigation-aware component NavigateCard to get proper keyboard handlers, or keep
Card and add accessibility: set role="button", tabIndex={0}, and implement key
event handling (onKeyDown) to call handleClick on Enter and Space, and ensure
ARIA labels are present to describe the action.

Copy link

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/pages/Manager/ManagedApplictaionDetail/index.tsx (1)

33-41: ⚠️ Potential issue | 🟡 Minor

Mutation 실패 시 에러 처리 추가 필요

useUpdateApplicationStatus, useDeleteApplication 훅에 onError 핸들러가 없어서, API 실패 시 사용자 피드백이 없습니다. 모달이 즉시 닫히므로 UX 혼란이 발생할 수 있습니다.

onError 콜백을 추가하여 에러 토스트를 표시하세요:

onError: (error) => {
  showToast('요청 처리에 실패했습니다');
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Manager/ManagedApplictaionDetail/index.tsx` around lines 33 - 41,
Add onError handlers to the mutation hooks so failures show a toast instead of
silently closing modals: update the useUpdateApplicationStatus and
useDeleteApplication calls (the mutations used by handleApprove and
handleReject) to include an onError callback that calls showToast('요청 처리에
실패했습니다') and prevents closing the modal when the mutation fails (i.e., only call
closeApprove/closeReject on success or in the mutation's onSuccess). This
ensures API errors are surfaced to the user and the modal stays open on failure.
src/pages/Auth/SignUp/ConfirmStep.tsx (1)

45-57: ⚠️ Potential issue | 🟡 Minor

문의 전송 실패 시 사용자 피드백이 빠져 있습니다.

submitInquiry의 실패 케이스에서 사용자에게 알림이 없어 무응답처럼 느껴질 수 있습니다. onError 핸들러를 추가하세요.

수정 예시
       {
         onSuccess: () => {
           closeInquiryModal();
           setInquiryContent('');
           showToast('문의가 접수되었습니다', 'success');
         },
+        onError: () => {
+          showToast('문의 접수에 실패했습니다. 다시 시도해주세요', 'error');
+        },
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Auth/SignUp/ConfirmStep.tsx` around lines 45 - 57, The submit flow
in handleInquirySubmit lacks user feedback on failure; add an onError handler to
the submitInquiry call (alongside the existing onSuccess) to call showToast with
a failure message (e.g., '문의 전송에 실패했습니다') and keep the modal/content state
unchanged (do not call closeInquiryModal or setInquiryContent('')), and include
the error message/details from the error argument in the toast or logs for
debugging.
🧹 Nitpick comments (5)
src/pages/Manager/ManagedApplictaionDetail/index.tsx (1)

15-17: params 유효성 검증 누락 가능성.

Number(params.clubId), Number(params.applicationId)NaN이 될 수 있습니다. 라우트 설정이 올바르다면 문제없지만, 방어적 코딩을 위해 유효성 검증을 고려해보세요.

♻️ 선택적 개선안
  const params = useParams();
  const clubId = Number(params.clubId);
  const applicationId = Number(params.applicationId);
+
+  if (Number.isNaN(clubId) || Number.isNaN(applicationId)) {
+    throw new Error('잘못된 접근입니다.');
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Manager/ManagedApplictaionDetail/index.tsx` around lines 15 - 17,
The code converting route params using useParams() may produce NaN for clubId
and applicationId; update the ManagedApplictaionDetail component to validate
params after const params = useParams() by checking Number(params.clubId) and
Number(params.applicationId) (or using parseInt) with isNaN checks and handle
invalid values (e.g., show an error UI, redirect, or return early) instead of
proceeding with NaN IDs; reference the params, clubId and applicationId
variables and ensure any downstream calls that use them only run when the values
are valid.
src/pages/User/MyPage/components/UserInfoCard.tsx (1)

44-67: 버튼에 type="button" 명시를 권장합니다.

클릭 핸들러만 있는 <button> 요소는 type="button"을 명시하는 것이 React 관례입니다. 기본 type이 submit이므로 의도를 명확히 합니다.

라인 58-64 (ManagerStats), 117-123, 124-130의 버튼들에 적용됩니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/User/MyPage/components/UserInfoCard.tsx` around lines 44 - 67, The
button elements in ManagerStats (and the other button instances noted) lack an
explicit type and default to submit; update each <button> used for
navigation/interaction (e.g., the button inside ManagerStats that calls
onButtonClick) to include type="button" to make the intent explicit and prevent
accidental form submissions—locate the button elements in ManagerStats and the
other button components and add the type="button" attribute to each.
src/pages/Club/Application/components/AccountInfo.tsx (2)

19-49: 불필요한 Fragment 제거 가능

<>...</> Fragment가 불필요합니다. Card 컴포넌트를 직접 반환할 수 있습니다.

♻️ Fragment 제거
   return (
-    <>
-      <Card className="gap-4">
+    <Card className="gap-4">
         ...
-      </Card>
-    </>
+    </Card>
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Club/Application/components/AccountInfo.tsx` around lines 19 - 49,
The top-level empty Fragment wrapping the Card in AccountInfo.tsx is
unnecessary; remove the outer <>...</> and return the <Card> directly (keep the
existing inner Fragment used for items.map as it provides the key and separator
logic). Ensure the component returns the Card element immediately and that JSX
is properly parenthesized after removal.

34-46: accountNumber가 null일 때 빈 문자열 복사 방지 고려

accountNumbernull일 경우 빈 문자열이 복사됩니다. 사용자에게 혼란을 줄 수 있으므로 복사 전 검증을 추가하는 것이 좋습니다.

♻️ null 체크 추가
 <button
   onClick={async () => {
+    if (!accountInfo.accountNumber) {
+      showToast('계좌번호가 없습니다');
+      return;
+    }
     try {
-      await navigator.clipboard.writeText(accountInfo.accountNumber ?? '');
+      await navigator.clipboard.writeText(accountInfo.accountNumber);
       showToast('계좌번호가 복사되었습니다');
     } catch {
       showToast('복사에 실패했습니다');
     }
   }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Club/Application/components/AccountInfo.tsx` around lines 34 - 46,
The copy button's onClick handler in the AccountInfo component attempts to write
accountInfo.accountNumber even when it's null, resulting in copying an empty
string; update the onClick logic to first validate that
accountInfo.accountNumber is a non-empty string (e.g., if
(!accountInfo.accountNumber) { showToast('계좌번호가 없습니다'); return; }), only call
navigator.clipboard.writeText when valid, and consider disabling the button or
changing its label when accountNumber is missing to prevent the action (refer to
the onClick handler, navigator.clipboard.writeText, and showToast).
src/pages/Manager/ManagedAccount/index.tsx (1)

56-62: 숫자 입력에 대한 유효성 검사 부재

amount 입력이 text 타입이지만 숫자만 허용해야 합니다. 현재 사용자가 문자를 입력해도 입력은 되지만 폼이 유효하지 않게 됩니다.

♻️ 숫자만 허용하도록 수정
 <input
-  type="text"
+  type="text"
+  inputMode="numeric"
   value={amount}
-  onChange={(e) => setAmount(e.target.value)}
+  onChange={(e) => {
+    const value = e.target.value.replace(/[^0-9]/g, '');
+    setAmount(value);
+  }}
   placeholder="가입비를 입력해주세요"
   className="bg-indigo-5 w-full rounded-lg p-2 text-[15px] leading-6 font-semibold"
 />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Manager/ManagedAccount/index.tsx` around lines 56 - 62, The amount
input currently allows any text; restrict it to numeric input by updating the
input handling for the amount field: either change the input to type="number"
and use setAmount(String(e.target.value)) or keep type="text" but validate
e.target.value in the onChange handler of the <input> tied to amount so that
only digits (and optionally a single decimal point) are accepted before calling
setAmount; ensure any stored amount remains a string for form compatibility and
consider trimming leading zeros or empty values to keep form validation
consistent with the amount and setAmount state logic.
🤖 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/ManagedAccount/index.tsx`:
- Around line 10-13: 현재 clubId를 Number(clubId)로 변환하면 clubId가 undefined일 때 NaN가
되어 useManagedClubFee 및 useManagedClubFeeMutation에 잘못된 값을 전달할 수 있으니 clubId가 존재하는지
검사하고 안전한 값이나 undefined를 전달하도록 변경하세요: clubId가 null/undefined이면 clubIdNumber를 계산하지
않고 대신 undefined(또는 적절한 기본값)를 사용하거나 컴포넌트에서 조기 반환 처리하여 useManagedClubFee 및
useManagedClubFeeMutation 호출을 건너뛰도록 하세요; 관련 식별자: clubIdNumber,
useManagedClubFee, useManagedClubFeeMutation.

---

Outside diff comments:
In `@src/pages/Auth/SignUp/ConfirmStep.tsx`:
- Around line 45-57: The submit flow in handleInquirySubmit lacks user feedback
on failure; add an onError handler to the submitInquiry call (alongside the
existing onSuccess) to call showToast with a failure message (e.g., '문의 전송에
실패했습니다') and keep the modal/content state unchanged (do not call
closeInquiryModal or setInquiryContent('')), and include the error
message/details from the error argument in the toast or logs for debugging.

In `@src/pages/Manager/ManagedApplictaionDetail/index.tsx`:
- Around line 33-41: Add onError handlers to the mutation hooks so failures show
a toast instead of silently closing modals: update the
useUpdateApplicationStatus and useDeleteApplication calls (the mutations used by
handleApprove and handleReject) to include an onError callback that calls
showToast('요청 처리에 실패했습니다') and prevents closing the modal when the mutation
fails (i.e., only call closeApprove/closeReject on success or in the mutation's
onSuccess). This ensures API errors are surfaced to the user and the modal stays
open on failure.

---

Duplicate comments:
In `@src/pages/User/MyPage/components/UserInfoCard.tsx`:
- Around line 27-39: The Card in UserInfoCard.tsx is clickable via onClick
(handleClick) but lacks keyboard accessibility; update the Card usage (or switch
to NavigateCard) to support keyboard activation by adding role="button",
tabIndex={0}, and an onKeyDown handler that triggers handleClick when Enter or
Space is pressed; ensure the accessible name (e.g., currentClub?.name 관리자)
remains readable and that RightArrowIcon remains present for visual affordance.

---

Nitpick comments:
In `@src/pages/Club/Application/components/AccountInfo.tsx`:
- Around line 19-49: The top-level empty Fragment wrapping the Card in
AccountInfo.tsx is unnecessary; remove the outer <>...</> and return the <Card>
directly (keep the existing inner Fragment used for items.map as it provides the
key and separator logic). Ensure the component returns the Card element
immediately and that JSX is properly parenthesized after removal.
- Around line 34-46: The copy button's onClick handler in the AccountInfo
component attempts to write accountInfo.accountNumber even when it's null,
resulting in copying an empty string; update the onClick logic to first validate
that accountInfo.accountNumber is a non-empty string (e.g., if
(!accountInfo.accountNumber) { showToast('계좌번호가 없습니다'); return; }), only call
navigator.clipboard.writeText when valid, and consider disabling the button or
changing its label when accountNumber is missing to prevent the action (refer to
the onClick handler, navigator.clipboard.writeText, and showToast).

In `@src/pages/Manager/ManagedAccount/index.tsx`:
- Around line 56-62: The amount input currently allows any text; restrict it to
numeric input by updating the input handling for the amount field: either change
the input to type="number" and use setAmount(String(e.target.value)) or keep
type="text" but validate e.target.value in the onChange handler of the <input>
tied to amount so that only digits (and optionally a single decimal point) are
accepted before calling setAmount; ensure any stored amount remains a string for
form compatibility and consider trimming leading zeros or empty values to keep
form validation consistent with the amount and setAmount state logic.

In `@src/pages/Manager/ManagedApplictaionDetail/index.tsx`:
- Around line 15-17: The code converting route params using useParams() may
produce NaN for clubId and applicationId; update the ManagedApplictaionDetail
component to validate params after const params = useParams() by checking
Number(params.clubId) and Number(params.applicationId) (or using parseInt) with
isNaN checks and handle invalid values (e.g., show an error UI, redirect, or
return early) instead of proceeding with NaN IDs; reference the params, clubId
and applicationId variables and ensure any downstream calls that use them only
run when the values are valid.

In `@src/pages/User/MyPage/components/UserInfoCard.tsx`:
- Around line 44-67: The button elements in ManagerStats (and the other button
instances noted) lack an explicit type and default to submit; update each
<button> used for navigation/interaction (e.g., the button inside ManagerStats
that calls onButtonClick) to include type="button" to make the intent explicit
and prevent accidental form submissions—locate the button elements in
ManagerStats and the other button components and add the type="button" attribute
to each.

Comment on lines +10 to +13
const clubIdNumber = Number(clubId);
const { managedClubFee } = useManagedClubFee(clubIdNumber);
const { banks } = useGetBanks();
const { mutate, isPending, error } = useManagedClubFeeMutation(Number(clubId));
const { mutate, isPending, error } = useManagedClubFeeMutation(clubIdNumber);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

clubId가 없을 때 NaN 전달 가능성

clubIdundefined일 경우 Number(undefined)NaN을 반환하며, 이 값이 훅에 전달되면 예기치 않은 동작이 발생할 수 있습니다.

🛡️ 제안 수정
 const { clubId } = useParams<{ clubId: string }>();
- const clubIdNumber = Number(clubId);
+ const clubIdNumber = Number(clubId);
+ if (Number.isNaN(clubIdNumber)) {
+   throw new Error('Invalid clubId');
+ }
📝 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 clubIdNumber = Number(clubId);
const { managedClubFee } = useManagedClubFee(clubIdNumber);
const { banks } = useGetBanks();
const { mutate, isPending, error } = useManagedClubFeeMutation(Number(clubId));
const { mutate, isPending, error } = useManagedClubFeeMutation(clubIdNumber);
const clubIdNumber = Number(clubId);
if (Number.isNaN(clubIdNumber)) {
throw new Error('Invalid clubId');
}
const { managedClubFee } = useManagedClubFee(clubIdNumber);
const { banks } = useGetBanks();
const { mutate, isPending, error } = useManagedClubFeeMutation(clubIdNumber);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Manager/ManagedAccount/index.tsx` around lines 10 - 13, 현재 clubId를
Number(clubId)로 변환하면 clubId가 undefined일 때 NaN가 되어 useManagedClubFee 및
useManagedClubFeeMutation에 잘못된 값을 전달할 수 있으니 clubId가 존재하는지 검사하고 안전한 값이나
undefined를 전달하도록 변경하세요: clubId가 null/undefined이면 clubIdNumber를 계산하지 않고 대신
undefined(또는 적절한 기본값)를 사용하거나 컴포넌트에서 조기 반환 처리하여 useManagedClubFee 및
useManagedClubFeeMutation 호출을 건너뛰도록 하세요; 관련 식별자: clubIdNumber,
useManagedClubFee, useManagedClubFeeMutation.

@ff1451 ff1451 merged commit e9c1f39 into develop Feb 19, 2026
1 check passed
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.

[refactor] 관리자 페이지 및 마이페이지 UI 수정 및 리팩토링

1 participant

Comments