Skip to content

[FE] 관리자 페이지 생성 및 api 연결 #245

Merged
discipline24 merged 14 commits intomainfrom
20260225_#242-기능추가관리자-관리자-페이지-회원-페이지-구현
Feb 26, 2026

Hidden character warning

The head ref may contain hidden characters: "20260225_#242-\uae30\ub2a5\ucd94\uac00\uad00\ub9ac\uc790-\uad00\ub9ac\uc790-\ud398\uc774\uc9c0-\ud68c\uc6d0-\ud398\uc774\uc9c0-\uad6c\ud604"
Merged

[FE] 관리자 페이지 생성 및 api 연결 #245
discipline24 merged 14 commits intomainfrom
20260225_#242-기능추가관리자-관리자-페이지-회원-페이지-구현

Conversation

@sangkyu39
Copy link
Contributor

@sangkyu39 sangkyu39 commented Feb 26, 2026

회원 관리 및 회원 가입 승인, 엑셀 업로드 기능 구현 완료

Summary by CodeRabbit

릴리스 노트

New Features

  • 관리자 대시보드 추가: 통계, 승인 대기, 최근 활동, 빠른 작업, 회원 목록 제공
  • 회원 승인 관리 추가: 검색·필터, 개별/일괄 승인 및 거절, 확인 모달 제공
  • 회원 관리 추가: 검색·필터, 권한·상태 변경, 선배 전환, 삭제 기능 제공
  • 엑셀 일괄 등록 추가: 드래그·파일 선택, 업로드 진행 표시 및 결과 보기 지원
  • 관리자 전용 라우트 보호 및 사이드바에 관리자 메뉴 노출

기타

  • 출석 페이지 데이터 동적 로드 및 로딩/오류 처리 개선

@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

Walkthrough

관리자 전용 라우트와 권한 검사(AdminRoute)를 추가하고, 관리자 대시보드, 회원 승인·관리, 엑셀 업로드 페이지 및 관련 컴포넌트·스타일·API 헬퍼들을 도입합니다. Sidebar에서 PRESIDENT 역할 검사로 관리자 메뉴를 조건부 노출하며 출석 데이터는 API 호출로 전환됩니다.

Changes

Cohort / File(s) Summary
라우팅·인증 변경
frontend/src/App.jsx, frontend/src/components/AdminRoute.jsx, frontend/src/components/Sidebar.jsx, frontend/src/contexts/AuthContext.jsx
관리자 전용 경로 추가 및 AdminRoute로 보호. Sidebar에 PRESIDENT 역할 검사 추가해 관리자 메뉴 조건부 노출. AuthContext에 ESLint 지시자 추가.
관리자 레이아웃 — 헤더·사이드바
frontend/src/components/AdminHome/AdminHeader.jsx, frontend/src/components/AdminHome/AdminHeader.module.css, frontend/src/components/AdminHome/AdminSidebar.jsx, frontend/src/components/AdminHome/AdminSidebar.module.css
관리자 헤더(검색/알림/유저) 및 탭 기반 사이드바 컴포넌트와 스타일 추가.
관리자 홈 페이지·위젯
frontend/src/pages/AdminHome.jsx, frontend/src/pages/AdminHome.module.css, frontend/src/components/AdminHome/DashboardStats.jsx, frontend/src/components/AdminHome/QuickActionsPanel.jsx, frontend/src/components/AdminHome/RecentActivitiesPanel.jsx, frontend/src/components/AdminHome/PendingApprovalsPanel.jsx, frontend/src/components/AdminHome/MembersPanel.jsx, frontend/src/components/AdminHome/MemberList.jsx, frontend/src/utils/adminHomeData.js
대시보드, 최근 활동, 빠른 작업, 대기 승인 및 멤버 리스트 위젯과 getAdminHomeData 유틸 추가.
회원 승인 기능
frontend/src/pages/AdminMemberApproval.jsx, frontend/src/pages/AdminMemberApproval.module.css, frontend/src/components/AdminMemberApproval/AdminMemberApproval.jsx, frontend/src/components/AdminMemberApproval/AdminMemberApproval.module.css, frontend/src/utils/adminMemberManageData.js
대기 회원 목록 조회·검색·선택·개별·일괄 승인·거절 흐름, 모달, 통계 카운터 및 관련 API 래퍼 추가.
회원 관리 기능
frontend/src/pages/AdminMemberManage.jsx, frontend/src/pages/AdminMemberManage.module.css, frontend/src/components/AdminMemberManage/AdminMemberManage.jsx, frontend/src/components/AdminMemberManage/AdminMemberManage.module.css, frontend/src/utils/adminMembersData.js, frontend/src/utils/adminUserApi.js
회원 검색·필터·권한·상태 변경, 선배 전환, 삭제, 엑셀 업로드 API 래퍼 등 멤버 관리 페이지 및 유틸 추가.
엑셀 업로드 UI
frontend/src/pages/AdminExcelUpload.jsx, frontend/src/pages/AdminExcelUpload.module.css, frontend/src/components/AdminExcelUpload/AdminExcelUpload.jsx, frontend/src/components/AdminExcelUpload/AdminExcelUpload.module.css, frontend/src/components/AdminExcelUpload/AdminExcelUploadHeader.jsx
드래그앤드롭·파일선택 기반 엑셀 업로드 컴포넌트, 템플릿 다운로드 액션, 업로드 결과 표시 UI 및 스타일 추가.
출석·세션 변경
frontend/src/components/attendance/SessionManage.jsx, frontend/src/utils/attendanceList.js, frontend/src/pages/Attendance.jsx
SessionManage에서 API로 세션 데이터 조회 추가. attendanceList를 정적 배열에서 API 호출 함수로 변경. Attendance에서 일부 컴포넌트(ExcusedTime) 사용 주석 처리.

Sequence Diagram(s)

sequenceDiagram
    participant User as 사용자
    participant AdminRoute as AdminRoute
    participant API as API Server
    participant AdminPage as 관리자 페이지

    User->>AdminRoute: /admin 접근
    AdminRoute->>API: GET /api/user/details
    API-->>AdminRoute: 사용자 정보

    alt 역할이 PRESIDENT
        AdminRoute->>AdminPage: <Outlet /> 렌더링
        AdminPage->>API: getAdminHomeData 등 관리자 데이터 요청
        API-->>AdminPage: 데이터 반환
        AdminPage-->>User: 관리자 UI 표시
    else 인증 실패 (401)
        AdminRoute-->>User: /login으로 리다이렉트
    else 권한 없음
        AdminRoute-->>User: /으로 리다이렉트
    end
Loading
sequenceDiagram
    participant Admin as 관리자
    participant UI as 승인 인터페이스
    participant API as API Server
    participant State as 로컬 상태

    Admin->>UI: 승인 페이지 로드
    UI->>API: getAdminMemberManageData()
    API-->>UI: pendingMembers 반환
    UI->>State: pendingMembers 저장
    UI-->>Admin: 테이블 렌더링

    Admin->>UI: 항목 선택(체크박스)
    UI->>State: selectedIds 업데이트
    Admin->>UI: 일괄 승인 클릭
    UI->>Admin: 확인 모달 표시
    Admin->>UI: 확인
    UI->>API: approvePendingMembersBulk({userIds})
    API-->>UI: 성공 응답
    UI->>State: 리스트 갱신
    UI-->>Admin: 성공 알림
Loading
sequenceDiagram
    participant Admin as 관리자
    participant UI as 엑셀 업로드 UI
    participant API as API Server
    participant State as 로컬 상태

    Admin->>UI: 파일 드래그/선택
    UI->>State: selectedFile 저장
    Admin->>UI: 업로드 버튼 클릭
    UI->>State: isUploading = true
    UI->>API: uploadAdminUsersExcel(file)
    API-->>UI: 업로드 결과 반환
    UI->>State: uploadResult 저장, isUploading = false
    UI-->>Admin: 결과 표시 및 토스트
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

FE

Suggested reviewers

  • DongEun02
  • gxuoo

Poem

🐰 관리실에 새 길이 뚫렸네,
엑셀도 받고 승인도 척척,
사이드바 길 따라 권한 검사,
대시보드엔 통계가 반짝반짝,
토끼가 뛰어와 축하해요! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 PR의 주요 변경 사항을 명확하게 설명합니다. 관리자 페이지 생성과 API 연결이라는 핵심 목표를 잘 요약하고 있습니다.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 20260225_#242-기능추가관리자-관리자-페이지-회원-페이지-구현

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.

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: 18

Caution

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

⚠️ Outside diff range comments (1)
frontend/src/contexts/AuthContext.jsx (1)

35-45: ⚠️ Potential issue | 🔴 Critical

로그아웃 성공 시 인증 상태가 해제되지 않습니다.

Line 35-45에서 logout API가 성공하면 isLoggedIn이 그대로 유지됩니다. 이후 보호 라우트 접근이 잘못 허용될 수 있습니다. setIsLoggedIn(false)는 성공/실패와 무관하게 항상 실행되도록 옮겨주세요.

수정 예시
 const logout = async () => {
   try {
     await api.post('/api/auth/logout');
   } catch (error) {
     // 로그아웃 API 실패해도 무시 (토큰이 없을 수 있음)
     console.log('로그아웃 API 호출 실패:', error.message);
-    setIsLoggedIn(false);
   } finally {
+    setIsLoggedIn(false);
     // localStorage 유저 정보 삭제
     localStorage.removeItem('user');
   }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/contexts/AuthContext.jsx` around lines 35 - 45, The logout
function currently only calls setIsLoggedIn(false) inside the catch, so on
successful API response the auth state remains true; move the
setIsLoggedIn(false) call into the finally block of logout (alongside
localStorage.removeItem('user')) so it always runs regardless of API success,
keep the console log in catch to record errors, and ensure you update references
in the logout function (setIsLoggedIn, api.post('/api/auth/logout'),
localStorage.removeItem) accordingly.
🧹 Nitpick comments (11)
frontend/src/components/Sidebar.jsx (3)

40-46: 역할 정보를 AuthContext에서 관리하는 것을 고려해보세요.

현재 Sidebar가 렌더링될 때마다 (그리고 isLoggedIn이 변경될 때마다) /api/user/details API를 호출합니다. 이 정보를 AuthContext에서 로그인 시 한 번 가져와 캐싱하면 불필요한 API 호출을 줄이고, 다른 컴포넌트에서도 역할 정보를 재사용할 수 있습니다.

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

In `@frontend/src/components/Sidebar.jsx` around lines 40 - 46, Sidebar currently
calls api.get('/api/user/details' ) on every render / isLoggedIn change to
compute setIsPresident; instead fetch and cache the user's role once in your
AuthContext during login (e.g., add a fetchUserDetails or loadUserRole method in
AuthContext that calls api.get('/api/user/details', normalizes role to uppercase
and stores it), and expose the role and an isPresident boolean from
AuthContext). Remove the api.get call and setIsPresident usage from Sidebar and
consume the cached role/isPresident from AuthContext (use the provided context
values and update any consumers to rely on AuthContext methods like
fetchUserDetails or role/isPresident).

1-1: ESLint 비활성화 지시어가 너무 광범위합니다.

파일 전체에 no-unused-vars 규칙을 비활성화하면 실제 미사용 변수를 놓칠 수 있습니다. 특정 라인에만 적용하거나, 미사용 변수를 제거하는 것이 좋습니다.

♻️ 제안된 수정
-/* eslint-disable no-unused-vars */

미사용 변수가 있다면 해당 라인에만 // eslint-disable-next-line no-unused-vars를 사용하거나, 변수를 제거해주세요.

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

In `@frontend/src/components/Sidebar.jsx` at line 1, Remove the file-level "/*
eslint-disable no-unused-vars */" in Sidebar.jsx and either delete the actual
unused variables/imports or replace the blanket disable with targeted line-level
suppressions (add "// eslint-disable-next-line no-unused-vars" immediately above
the specific import/variable declaration). Locate the unused symbols in the
Sidebar component (imports or local variables inside Sidebar or its helper
functions) and apply the narrow inline disable only to those lines or remove
them entirely.

33-50: 비동기 요청에 대한 cleanup 처리가 누락되었습니다.

컴포넌트가 언마운트되거나 isLoggedIn이 빠르게 변경될 경우, 이전 요청이 완료된 후 상태를 업데이트하려고 시도하여 메모리 누수나 race condition이 발생할 수 있습니다. AbortController를 사용하여 cleanup 처리를 추가하는 것을 권장합니다.

♻️ AbortController를 사용한 cleanup 처리
 useEffect(() => {
+  const controller = new AbortController();
+
   const checkAdminRole = async () => {
     if (!isLoggedIn) {
       setIsPresident(false);
       return;
     }

     try {
-      const { data } = await api.get('/api/user/details');
+      const { data } = await api.get('/api/user/details', {
+        signal: controller.signal,
+      });
       const normalizedRole = String(data?.role || '').trim().toUpperCase();
       setIsPresident(normalizedRole === 'PRESIDENT');
-    } catch {
-      setIsPresident(false);
+    } catch (error) {
+      if (!controller.signal.aborted) {
+        setIsPresident(false);
+      }
     }
   };

   checkAdminRole();
+
+  return () => {
+    controller.abort();
+  };
 }, [isLoggedIn]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/Sidebar.jsx` around lines 33 - 50, The effect's async
checkAdminRole can update state after unmount or rapid isLoggedIn changes;
create an AbortController inside useEffect, pass its signal to
api.get('/api/user/details') call in checkAdminRole, and in the catch ignore or
detect abort errors so you don't call setIsPresident when aborted; ensure the
effect returns a cleanup that calls controller.abort() and check the
controller.signal before calling setIsPresident in checkAdminRole to avoid race
conditions.
frontend/src/utils/adminMembersData.js (1)

1-38: adminUserApi와 기능 중복이 커서 단일 API 모듈로 통합하는 편이 안전합니다.

동일 엔드포인트/동일 행위가 두 유틸(frontend/src/utils/adminMembersData.js, frontend/src/utils/adminUserApi.js)에 분산되어 있어 수정 누락 위험이 큽니다. 공통 API를 한 곳으로 합치고 화면별 어댑터만 두는 구조를 권장합니다.

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

In `@frontend/src/utils/adminMembersData.js` around lines 1 - 38, This module
duplicates APIs already implemented in adminUserApi; consolidate by removing
these duplicated functions (getAdminMembersData, changeAdminMemberRole,
changeAdminMemberStatus, promoteAdminMemberSenior, deleteAdminMember) and
refactor callers to import and use the single source of truth in adminUserApi,
keeping only lightweight adapters in this file if UI-specific parameter mapping
is required; ensure method signatures and parameter names match adminUserApi and
update any imports/usages across the codebase to prevent runtime breakage.
frontend/src/contexts/AuthContext.jsx (1)

26-29: paylaod 오타를 정리하면 가독성이 좋아집니다.

Line 27 변수명 오타(paylaod)는 동작엔 문제없지만 추후 탐색/유지보수 시 혼동을 만듭니다.

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

In `@frontend/src/contexts/AuthContext.jsx` around lines 26 - 29, Fix the typo in
the login function by renaming the local variable paylaod to payload and update
its usage in the api.post call; locate the async function login ({ studentId,
password }, signal) in AuthContext.jsx and replace the misspelled variable name
paylaod with payload so the object declaration and the argument passed to
api.post('/api/auth/login', ...) are consistent.
frontend/src/utils/adminMemberManageData.js (2)

35-43: 대량 작업 시 부분 실패(partial failure) 처리 고려 필요

Promise.all은 하나의 요청이 실패하면 즉시 reject되지만, 이미 성공한 요청들은 롤백되지 않습니다. 예를 들어 5명 중 3명이 승인된 후 4번째에서 실패하면, 3명은 승인된 상태로 남지만 사용자에게는 전체 실패로 표시됩니다.

Promise.allSettled를 사용하거나 백엔드에 일괄 처리 API를 추가하는 것을 권장합니다.

♻️ Promise.allSettled 사용 예시
 export const approvePendingMembersBulk = async ({ userIds }) => {
-  await Promise.all(
+  const results = await Promise.allSettled(
     userIds.map((userId) =>
       api.patch(`/api/admin/users/${userId}/role`, null, {
         params: { role: 'TEAM_MEMBER' },
       })
     )
   );
+  const failed = results.filter((r) => r.status === 'rejected');
+  if (failed.length > 0) {
+    throw new Error(`${userIds.length - failed.length}명 승인 성공, ${failed.length}명 실패`);
+  }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/utils/adminMemberManageData.js` around lines 35 - 43, The
approvePendingMembersBulk function currently uses Promise.all which rejects on
the first failure causing misleading results; change it to use
Promise.allSettled inside approvePendingMembersBulk to run all PATCH requests,
then inspect each result to build a summary of succeeded and failed userIds
(refer to approvePendingMembersBulk and the api.patch calls), return or throw a
structured result that contains arrays of successes and failures (and include
error messages for failures) so the caller can display partial-success
information or retry failed IDs.

13-17: 월별 통계 값이 하드코딩됨

monthlyApprovedCountmonthlyRejectedCount가 항상 0으로 반환됩니다. 백엔드 API 응답에서 해당 값을 제공한다면 파싱하여 반환하거나, TODO 주석을 추가하여 추후 구현 예정임을 명시하는 것이 좋습니다.

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

In `@frontend/src/utils/adminMemberManageData.js` around lines 13 - 17, The
returned object in getAdminMemberManageData (or the function containing this
return) hardcodes monthlyApprovedCount and monthlyRejectedCount to 0; update the
return to pull these values from the API response (e.g.,
response.data.monthlyApprovedCount and response.data.monthlyRejectedCount) with
safe fallbacks (|| 0) so they reflect backend data, or if the API doesn't yet
provide them, add a TODO comment near monthlyApprovedCount/monthlyRejectedCount
indicating the fields must be implemented by the backend and kept as 0 only
temporarily.
frontend/src/components/AdminHome/PendingApprovalsPanel.jsx (2)

42-67: 빈 목록에 대한 UI 처리 고려

members 배열이 비어있을 때 빈 <ul> 요소만 렌더링됩니다. 사용자에게 "대기 중인 회원이 없습니다" 등의 안내 메시지를 표시하면 UX가 개선됩니다.

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

In `@frontend/src/components/AdminHome/PendingApprovalsPanel.jsx` around lines 42
- 67, PendingApprovalsPanel currently renders an empty <ul> when the members
array is empty; update the component render logic around the members.map (where
it builds the <ul className={styles.list}> with items keyed by member.id) to
conditionally show a user-friendly message (e.g., "대기 중인 회원이 없습니다") when
members.length === 0 and only render the <ul> and its mapped <li> items when
members.length > 0; ensure the empty-state text uses an appropriate CSS class
(e.g., styles.empty) for styling and remains accessible.

16-18: Axios 에러에서 사용자 친화적 메시지 추출 개선

Axios 에러의 경우 error.message는 일반적인 네트워크 에러 메시지일 수 있고, 서버 응답 메시지는 error.response?.data?.message에 위치합니다.

♻️ 에러 메시지 추출 개선 예시
     } catch (error) {
-      window.alert(error?.message || '가입 승인 처리에 실패했습니다.');
+      window.alert(error?.response?.data?.message || error?.message || '가입 승인 처리에 실패했습니다.');
     }

Also applies to: 29-31

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

In `@frontend/src/components/AdminHome/PendingApprovalsPanel.jsx` around lines 16
- 18, Update the catch handlers in PendingApprovalsPanel.jsx (the catch blocks
that currently call window.alert(error?.message || '가입 승인 처리에 실패했습니다.')) to
extract a user-friendly message from Axios responses by checking
error.response?.data?.message first, then falling back to error.message and
finally a default Korean message; either add a small helper function like
getErrorMessage(error) used by both catch blocks around the approval/rejection
functions or inline the same three-way fallback (error.response?.data?.message
|| error?.message || '가입 승인 처리에 실패했습니다.') before passing it to window.alert to
ensure server-provided messages are shown when available.
frontend/src/components/AdminMemberApproval/AdminMemberApproval.jsx (1)

258-262: 신청일시 컬럼이 하드코딩된 "-"로 표시됨

member 객체에 신청일시 데이터가 있다면 해당 값을 표시해야 합니다. 의도적으로 생략한 것이라면 TODO 주석을 추가하는 것이 좋습니다.

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

In `@frontend/src/components/AdminMemberApproval/AdminMemberApproval.jsx` around
lines 258 - 262, The date cell currently shows a hardcoded "-"; replace that
with the member's application timestamp (e.g., use member.appliedAt or the
appropriate field on the member object such as
member.requestedAt/member.createdAt) and render a human-readable value (e.g.,
new Date(member.appliedAt).toLocaleString() or your project's date formatter).
If the omission was intentional, add a TODO comment explaining why and include
when/where the real value will be added. Update the JSX inside the dateBox (the
<div className={styles.dateBox}> containing <Clock />) to use the member
property and formatted output.
frontend/src/components/AdminHome/AdminSidebar.module.css (1)

146-149: 모바일에서 사이드바를 완전히 숨기는 방식은 재검토가 필요합니다.

현재 방식은 작은 화면에서 관리자 메뉴 진입 경로를 없앨 수 있습니다. 접이식/드로어 형태 등 모바일 대체 내비게이션을 남기는 쪽이 안전합니다.

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

In `@frontend/src/components/AdminHome/AdminSidebar.module.css` around lines 146 -
149, 현재 media query (max-width:1023px)에서 .sidebar를 display:none으로 완전히 숨기지 말고
모바일용 접이식 네비게이션을 남기도록 수정하세요: AdminSidebar.module.css의 해당 미디어 쿼리에서 display:none을
제거하고 대신 모바일 상태용 클래스(.sidebar--closed / .sidebar--open 또는 .is-open)를 사용해 오른쪽/왼쪽
슬라이드 드로어 스타일(예: transform/translateX, width 또는 max-height 제어)로 숨김 처리하고, 항상 보이는
토글 버튼(.sidebarToggle 또는 .hamburger)을 유지해 토글 시 .sidebar에 .is-open 클래스를 붙여 열고 닫히게
하며 ARIA 속성(aria-hidden 또는 aria-expanded)과 포커스 관리에 맞춰 JavaScript 처리(토글 제어함수)를
연결하도록 변경하세요.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/components/AdminExcelUpload/AdminExcelUpload.jsx`:
- Line 1: The file input in AdminExcelUpload.jsx doesn't clear its value after
handling a selection, so re-selecting the same file won't fire onChange; update
the file input handlers in the AdminExcelUpload component (the onChange handlers
for the file inputs referenced around the blocks at 34-37, 74-77, and 104-109)
to reset the input value after processing — either by setting e.target.value =
'' at the end of the handler or by using a ref to the <input type="file"> and
assigning ref.current.value = '' so subsequent identical selections trigger
onChange again.

In `@frontend/src/components/AdminExcelUpload/AdminExcelUpload.module.css`:
- Around line 128-134: The .fileName CSS class can still overflow in flex
layouts because the flex item needs an explicit min-width to allow
text-overflow: ellipsis to work; update the .fileName rule (used where file
names are rendered) to include min-width: 0 (and/or max-width as appropriate) so
the element can shrink inside a flex container and reliably apply
overflow:hidden and text-overflow:ellipsis.

In `@frontend/src/components/AdminHome/AdminHeader.jsx`:
- Around line 15-19: The search input in AdminHeader.jsx lacks an accessible
name; update the input element (in the AdminHeader component, currently using
className={styles.searchInput}) to provide an explicit accessible label by
either adding an appropriate aria-label (e.g. aria-label="검색" or "Search") or
connecting a <label> element to the input (give the input an id and render a
visually-hidden label). Make sure the label text is localized and descriptive,
and remove reliance on placeholder text alone so screen readers correctly
announce the field.

In `@frontend/src/components/AdminHome/AdminHeader.module.css`:
- Around line 49-56: The .searchInput rule currently removes the browser focus
indicator with `outline: none`, causing an accessibility issue; update the CSS
for the .searchInput selector to provide a visible, accessible focus style
instead of removing it (for example use :focus or :focus-visible to add a clear
outline or ring/box-shadow and keep contrast), ensuring keyboard users can
identify focus while preserving the existing sizing and layout in
AdminHeader.module.css.

In `@frontend/src/components/AdminHome/AdminSidebar.jsx`:
- Around line 30-40: The sidebar currently exposes links to unimplemented routes
(e.g., items with hrefs '/admin/posts', '/admin/attendance', '/admin/points',
'/admin/tools', '/admin/dashboard' in the AdminSidebar.jsx items arrays),
causing 404/empty pages; update AdminSidebar.jsx to hide or disable those
entries until corresponding App routes exist by either removing or marking those
item objects as disabled/hidden (add a flag like disabled: true or exclude them
from the rendered items) and ensure the rendering logic in the AdminSidebar
component respects that flag for all items arrays in this file so the same
treatment applies to the other entries referenced in the review.

In `@frontend/src/components/AdminMemberApproval/AdminMemberApproval.jsx`:
- Line 248: Replace the unsafe access member.name[0] in the avatar rendering
with a guarded expression so it won't throw when name is undefined/empty: use
optional chaining and a fallback (for example member?.name?.[0] ?? '' or
member?.name?.charAt(0) || '?') in the JSX inside AdminMemberApproval.jsx where
the div with className={styles.avatar} is rendered to ensure a safe character or
a placeholder is shown.
- Around line 100-121: confirmApprove currently defines an async helper
approveAction and calls it without awaiting which can cause unhandled rejections
and state updates after unmount; make confirmApprove itself async (or await the
promise returned by approveAction) and await the approve flow so errors are
caught and state updates occur synchronously in the try/catch; apply the same
fix to confirmReject (ensure the async logic inside confirmReject is awaited or
converted so confirmReject is async) and keep the existing try/catch/error
handling around approvePendingMember/approvePendingMembersBulk and
loadPendingMembers to properly handle failures.

In `@frontend/src/components/AdminMemberManage/AdminMemberManage.jsx`:
- Around line 157-189: The promote/delete handlers lack in-flight guards so
rapid clicks send duplicate PATCH/DELETE requests; add boolean state flags
(e.g., isPromotingSenior, isDeletingMember or a generic inFlight map) and set
the appropriate flag true at the start of handlePromoteSenior/handleDelete (and
the other handlers around lines 281-308), early-return if the flag is already
true, await promoteAdminMemberSenior/deleteAdminMember, then clear the flag in a
finally block; also use those flags to disable the corresponding promote/delete
buttons in the component render so UI prevents repeated submissions.
- Line 240: The table header currently reads "가입일" but the cells render
generation (기수); update AdminMemberManage component to make header and data
consistent: either change the <th> text from "가입일" to "기수" (both occurrences) to
match the rendered value (e.g., where rows render member.generation), or change
the cell rendering to use the actual join date field (e.g., member.joinDate /
member.createdAt / member.joinedAt) and keep the header as "가입일"; apply the same
fix at both locations referenced (the <th> at the first occurrence and the
second occurrence around the row rendering).
- Line 249: The avatar initial rendering uses member.name[0], which will throw
if member or member.name is null/empty; update the AdminMemberManage.jsx
rendering logic (the div with className styles.avatar) to safely access the
first character by checking member and member.name and falling back to a safe
default (e.g., an empty string or placeholder character) when name is missing —
use a null-safe access pattern for member and member.name and derive the initial
from that safe string before rendering.
- Line 71: Remove the unsafe console.log that prints nextMembers in
AdminMemberManage.jsx to avoid leaking PII; either delete the console.log('회원 목록
로드 성공:', nextMembers) entirely or replace it with a non-sensitive debug message
(e.g., count only) and, if needed, ensure any logging sanitizes or redacts
fields like email/studentId before logging. Locate the statement referencing
nextMembers and update the code around that load/handler function to stop
outputting full member objects to console.
- Around line 78-91: Remove the redundant initial useEffect that calls
loadMembers() with an empty dependency array; keep the existing
useEffect([roleFilter, searchQuery, statusFilter]) as the single source of truth
so the member list is loaded on mount and whenever roleFilter, searchQuery, or
statusFilter change. Ensure the preserved useEffect calls loadMembers({ keyword:
searchQuery.trim() || undefined, role: roleFilter === 'all' ? undefined :
roleFilter, status: statusFilter === 'all' ? undefined : statusFilter }) so
behavior matches the original API parameters.

In `@frontend/src/components/AdminMemberManage/AdminMemberManage.module.css`:
- Around line 41-51: The table wrapper (.tableWrap) currently uses overflow:
hidden which prevents horizontal scrolling and causes columns to be clipped on
narrow screens; change the overflow rule to allow horizontal scrolling (e.g.,
use overflow-x: auto or overflow: auto) and ensure the table (.table) can scroll
by preserving its full width (optionally set a min-width or avoid forcing
collapse) so wide tables become horizontally scrollable on small viewports.

In `@frontend/src/pages/AdminHome.jsx`:
- Around line 25-28: loadAdminHomeData currently awaits getAdminHomeData without
error handling so API failures will bubble up and break rendering; wrap the call
in a try/catch inside loadAdminHomeData, call setData only on success, log or
surface the error (e.g. process/error logger or set an error state like
setError) and optionally setData to a safe fallback in the catch block; update
references to getAdminHomeData, loadAdminHomeData, setData (and add setError if
needed) so the component gracefully handles API failures and preserves UI flow.

In `@frontend/src/utils/adminHomeData.js`:
- Around line 18-23: The code assumes response.data is an array which can cause
"users.filter is not a function"; update the users assignment (where response
and users are used) to defensively ensure an array before calling filter (e.g.,
set users = Array.isArray(response.data) ? response.data : [] or coerce a valid
array from response.data.payload/response.data.users as needed), then keep
computing pendingApprovals and members from that guaranteed-array variable
(referencing response, users, pendingApprovals, members).

In `@frontend/src/utils/adminMembersData.js`:
- Around line 17-38: Add pre-validation for userId in all admin member API
helpers to mirror adminUserApi: in changeAdminMemberRole,
changeAdminMemberStatus, promoteAdminMemberSenior, and deleteAdminMember check
that the supplied userId is present (and throw or return a clear error) before
calling api.patch/api.delete so you never call an invalid path like
`/api/admin/users/undefined/...`; follow the same validation style/pattern used
in adminUserApi for consistency.

In `@frontend/src/utils/adminUserApi.js`:
- Around line 41-44: In uploadAdminUsersExcel, validate the incoming file before
creating the FormData and sending the request: check that the parameter file is
present and is a File/Blob (and optionally enforce size/type rules) and return
or throw a clear error (or reject the promise) if validation fails to avoid
making the network call; update the function (export const
uploadAdminUsersExcel) to perform this guard early and only build the FormData
and proceed with the request when the file passes validation.
- Around line 17-38: Add a guard that validates userId before making any API
call to avoid requests to /api/admin/users/undefined; for each exported function
(updateAdminUserRole, updateAdminUserStatus, promoteAdminUserSenior,
deleteAdminUser) check that the userId argument is present and of the expected
type (non-empty string or number) and if not immediately throw a clear error
(e.g., "userId is required") or reject the promise so the caller fails fast
instead of calling api.patch/api.delete with an invalid path.

---

Outside diff comments:
In `@frontend/src/contexts/AuthContext.jsx`:
- Around line 35-45: The logout function currently only calls
setIsLoggedIn(false) inside the catch, so on successful API response the auth
state remains true; move the setIsLoggedIn(false) call into the finally block of
logout (alongside localStorage.removeItem('user')) so it always runs regardless
of API success, keep the console log in catch to record errors, and ensure you
update references in the logout function (setIsLoggedIn,
api.post('/api/auth/logout'), localStorage.removeItem) accordingly.

---

Nitpick comments:
In `@frontend/src/components/AdminHome/AdminSidebar.module.css`:
- Around line 146-149: 현재 media query (max-width:1023px)에서 .sidebar를
display:none으로 완전히 숨기지 말고 모바일용 접이식 네비게이션을 남기도록 수정하세요: AdminSidebar.module.css의
해당 미디어 쿼리에서 display:none을 제거하고 대신 모바일 상태용 클래스(.sidebar--closed / .sidebar--open
또는 .is-open)를 사용해 오른쪽/왼쪽 슬라이드 드로어 스타일(예: transform/translateX, width 또는
max-height 제어)로 숨김 처리하고, 항상 보이는 토글 버튼(.sidebarToggle 또는 .hamburger)을 유지해 토글 시
.sidebar에 .is-open 클래스를 붙여 열고 닫히게 하며 ARIA 속성(aria-hidden 또는 aria-expanded)과 포커스
관리에 맞춰 JavaScript 처리(토글 제어함수)를 연결하도록 변경하세요.

In `@frontend/src/components/AdminHome/PendingApprovalsPanel.jsx`:
- Around line 42-67: PendingApprovalsPanel currently renders an empty <ul> when
the members array is empty; update the component render logic around the
members.map (where it builds the <ul className={styles.list}> with items keyed
by member.id) to conditionally show a user-friendly message (e.g., "대기 중인 회원이
없습니다") when members.length === 0 and only render the <ul> and its mapped <li>
items when members.length > 0; ensure the empty-state text uses an appropriate
CSS class (e.g., styles.empty) for styling and remains accessible.
- Around line 16-18: Update the catch handlers in PendingApprovalsPanel.jsx (the
catch blocks that currently call window.alert(error?.message || '가입 승인 처리에
실패했습니다.')) to extract a user-friendly message from Axios responses by checking
error.response?.data?.message first, then falling back to error.message and
finally a default Korean message; either add a small helper function like
getErrorMessage(error) used by both catch blocks around the approval/rejection
functions or inline the same three-way fallback (error.response?.data?.message
|| error?.message || '가입 승인 처리에 실패했습니다.') before passing it to window.alert to
ensure server-provided messages are shown when available.

In `@frontend/src/components/AdminMemberApproval/AdminMemberApproval.jsx`:
- Around line 258-262: The date cell currently shows a hardcoded "-"; replace
that with the member's application timestamp (e.g., use member.appliedAt or the
appropriate field on the member object such as
member.requestedAt/member.createdAt) and render a human-readable value (e.g.,
new Date(member.appliedAt).toLocaleString() or your project's date formatter).
If the omission was intentional, add a TODO comment explaining why and include
when/where the real value will be added. Update the JSX inside the dateBox (the
<div className={styles.dateBox}> containing <Clock />) to use the member
property and formatted output.

In `@frontend/src/components/Sidebar.jsx`:
- Around line 40-46: Sidebar currently calls api.get('/api/user/details' ) on
every render / isLoggedIn change to compute setIsPresident; instead fetch and
cache the user's role once in your AuthContext during login (e.g., add a
fetchUserDetails or loadUserRole method in AuthContext that calls
api.get('/api/user/details', normalizes role to uppercase and stores it), and
expose the role and an isPresident boolean from AuthContext). Remove the api.get
call and setIsPresident usage from Sidebar and consume the cached
role/isPresident from AuthContext (use the provided context values and update
any consumers to rely on AuthContext methods like fetchUserDetails or
role/isPresident).
- Line 1: Remove the file-level "/* eslint-disable no-unused-vars */" in
Sidebar.jsx and either delete the actual unused variables/imports or replace the
blanket disable with targeted line-level suppressions (add "//
eslint-disable-next-line no-unused-vars" immediately above the specific
import/variable declaration). Locate the unused symbols in the Sidebar component
(imports or local variables inside Sidebar or its helper functions) and apply
the narrow inline disable only to those lines or remove them entirely.
- Around line 33-50: The effect's async checkAdminRole can update state after
unmount or rapid isLoggedIn changes; create an AbortController inside useEffect,
pass its signal to api.get('/api/user/details') call in checkAdminRole, and in
the catch ignore or detect abort errors so you don't call setIsPresident when
aborted; ensure the effect returns a cleanup that calls controller.abort() and
check the controller.signal before calling setIsPresident in checkAdminRole to
avoid race conditions.

In `@frontend/src/contexts/AuthContext.jsx`:
- Around line 26-29: Fix the typo in the login function by renaming the local
variable paylaod to payload and update its usage in the api.post call; locate
the async function login ({ studentId, password }, signal) in AuthContext.jsx
and replace the misspelled variable name paylaod with payload so the object
declaration and the argument passed to api.post('/api/auth/login', ...) are
consistent.

In `@frontend/src/utils/adminMemberManageData.js`:
- Around line 35-43: The approvePendingMembersBulk function currently uses
Promise.all which rejects on the first failure causing misleading results;
change it to use Promise.allSettled inside approvePendingMembersBulk to run all
PATCH requests, then inspect each result to build a summary of succeeded and
failed userIds (refer to approvePendingMembersBulk and the api.patch calls),
return or throw a structured result that contains arrays of successes and
failures (and include error messages for failures) so the caller can display
partial-success information or retry failed IDs.
- Around line 13-17: The returned object in getAdminMemberManageData (or the
function containing this return) hardcodes monthlyApprovedCount and
monthlyRejectedCount to 0; update the return to pull these values from the API
response (e.g., response.data.monthlyApprovedCount and
response.data.monthlyRejectedCount) with safe fallbacks (|| 0) so they reflect
backend data, or if the API doesn't yet provide them, add a TODO comment near
monthlyApprovedCount/monthlyRejectedCount indicating the fields must be
implemented by the backend and kept as 0 only temporarily.

In `@frontend/src/utils/adminMembersData.js`:
- Around line 1-38: This module duplicates APIs already implemented in
adminUserApi; consolidate by removing these duplicated functions
(getAdminMembersData, changeAdminMemberRole, changeAdminMemberStatus,
promoteAdminMemberSenior, deleteAdminMember) and refactor callers to import and
use the single source of truth in adminUserApi, keeping only lightweight
adapters in this file if UI-specific parameter mapping is required; ensure
method signatures and parameter names match adminUserApi and update any
imports/usages across the codebase to prevent runtime breakage.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 99e8e0f and 8cddcaf.

📒 Files selected for processing (33)
  • frontend/src/App.jsx
  • frontend/src/components/AdminExcelUpload/AdminExcelUpload.jsx
  • frontend/src/components/AdminExcelUpload/AdminExcelUpload.module.css
  • frontend/src/components/AdminExcelUpload/AdminExcelUploadHeader.jsx
  • frontend/src/components/AdminHome/AdminHeader.jsx
  • frontend/src/components/AdminHome/AdminHeader.module.css
  • frontend/src/components/AdminHome/AdminSidebar.jsx
  • frontend/src/components/AdminHome/AdminSidebar.module.css
  • frontend/src/components/AdminHome/DashboardStats.jsx
  • frontend/src/components/AdminHome/MemberList.jsx
  • frontend/src/components/AdminHome/MembersPanel.jsx
  • frontend/src/components/AdminHome/PendingApprovalsPanel.jsx
  • frontend/src/components/AdminHome/QuickActionsPanel.jsx
  • frontend/src/components/AdminHome/RecentActivitiesPanel.jsx
  • frontend/src/components/AdminMemberApproval/AdminMemberApproval.jsx
  • frontend/src/components/AdminMemberApproval/AdminMemberApproval.module.css
  • frontend/src/components/AdminMemberManage/AdminMemberManage.jsx
  • frontend/src/components/AdminMemberManage/AdminMemberManage.module.css
  • frontend/src/components/AdminRoute.jsx
  • frontend/src/components/Sidebar.jsx
  • frontend/src/contexts/AuthContext.jsx
  • frontend/src/pages/AdminExcelUpload.jsx
  • frontend/src/pages/AdminExcelUpload.module.css
  • frontend/src/pages/AdminHome.jsx
  • frontend/src/pages/AdminHome.module.css
  • frontend/src/pages/AdminMemberApproval.jsx
  • frontend/src/pages/AdminMemberApproval.module.css
  • frontend/src/pages/AdminMemberManage.jsx
  • frontend/src/pages/AdminMemberManage.module.css
  • frontend/src/utils/adminHomeData.js
  • frontend/src/utils/adminMemberManageData.js
  • frontend/src/utils/adminMembersData.js
  • frontend/src/utils/adminUserApi.js

discipline24
discipline24 previously approved these changes Feb 26, 2026
Copy link
Contributor

@discipline24 discipline24 left a comment

Choose a reason for hiding this comment

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

코드양이 엄청많네요 고생하셨습니다~

@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

Note

Docstrings generation - SUCCESS
Generated docstrings for this pull request at #248

coderabbitai bot added a commit that referenced this pull request Feb 26, 2026
Docstrings generation was requested by @sangkyu39.

* #245 (comment)

The following files were modified:

* `frontend/src/App.jsx`
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: 6

♻️ Duplicate comments (1)
frontend/src/components/AdminMemberApproval/AdminMemberApproval.jsx (1)

56-60: ⚠️ Potential issue | 🟠 Major

검색 필드 접근에 null/type 방어가 필요합니다.

name/email/studentId 중 하나라도 비어 있거나 문자열이 아니면 필터링에서 런타임 에러가 납니다.

수정 예시
-    return pendingMembers.filter(
-      (member) =>
-        member.name.toLowerCase().includes(normalizedQuery) ||
-        member.email.toLowerCase().includes(normalizedQuery) ||
-        member.studentId.includes(normalizedQuery)
-    );
+    return pendingMembers.filter((member) => {
+      const name = String(member?.name ?? '').toLowerCase();
+      const email = String(member?.email ?? '').toLowerCase();
+      const studentId = String(member?.studentId ?? '').toLowerCase();
+      return (
+        name.includes(normalizedQuery) ||
+        email.includes(normalizedQuery) ||
+        studentId.includes(normalizedQuery)
+      );
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/AdminMemberApproval/AdminMemberApproval.jsx` around
lines 56 - 60, The filter callback in pendingMembers.filter uses
member.name/member.email/member.studentId without null/type checks which can
throw if any field is missing or not a string; update the callback (referencing
pendingMembers.filter, member.name, member.email, member.studentId,
normalizedQuery) to defensively coerce or default each field to a string before
using toLowerCase/includes (e.g., use optional chaining or fallback to '' and
String(...) then toLowerCase for name/email and String(...) for studentId) so
the includes checks never run on null/undefined/non-string values.
🧹 Nitpick comments (7)
frontend/src/components/AdminExcelUpload/AdminExcelUpload.module.css (2)

190-199: 결과 영역 높이에 상한을 두는 편이 안전합니다.

Line 190~199에서 overflow: auto는 적절하지만, 매우 긴 업로드 결과가 들어오면 섹션이 과도하게 커질 수 있습니다. max-height를 추가해 스크롤 영역으로 고정하는 구성을 권장합니다.

개선 예시
 .resultBox {
   margin: 0;
   border: 1px solid `#e5e7eb`;
   border-radius: 8px;
   background: `#f9fafb`;
   padding: 12px;
+  max-height: 320px;
   overflow: auto;
   font-size: 12px;
   line-height: 1.5;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/AdminExcelUpload/AdminExcelUpload.module.css` around
lines 190 - 199, The .resultBox style allows overflow but lacks a size cap; add
a max-height (e.g., max-height: 40vh or a fixed px value) to the .resultBox rule
and keep overflow: auto so very long upload results become scrollable instead of
expanding the section; adjust the exact max-height to match the UI (or use
calc() with header/footer heights) to ensure consistent layout.

169-176: 회전 애니메이션에 prefers-reduced-motion 대응을 추가해 주세요.

Line 169~176의 무한 회전은 모션 민감 사용자에게 부담이 될 수 있습니다. 접근성 측면에서 모션 축소 환경을 처리하는 것이 좋습니다.

개선 예시
 .spin {
   animation: spin 1s linear infinite;
 }
 
 `@keyframes` spin {
   from { transform: rotate(0deg); }
   to { transform: rotate(360deg); }
 }
+
+@media (prefers-reduced-motion: reduce) {
+  .spin {
+    animation: none;
+  }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/AdminExcelUpload/AdminExcelUpload.module.css` around
lines 169 - 176, The infinite rotation defined by the .spin class and `@keyframes`
spin must respect users' prefers-reduced-motion setting: update the CSS to
disable or remove the animation when `@media` (prefers-reduced-motion: reduce)
applies (e.g., set animation: none on .spin and avoid running keyframes) so
motion-sensitive users do not see the spinning animation; make this change
around the .spin class and `@keyframes` spin definitions to ensure the animation
is overridden for reduced-motion preferences.
frontend/src/components/AdminMemberManage/AdminMemberManage.jsx (1)

83-87: 목록 재조회 파라미터 구성이 중복되어 유지보수 리스크가 있습니다.

동일한 keyword/role/status 계산 로직이 여러 곳에 반복됩니다. 공통 헬퍼로 추출하면 향후 필터 변경 시 누락 가능성을 줄일 수 있습니다.

♻️ 리팩터링 예시
+  const buildMemberQuery = () => ({
+    keyword: searchQuery.trim() || undefined,
+    role: roleFilter === 'all' ? undefined : roleFilter,
+    status: statusFilter === 'all' ? undefined : statusFilter,
+  });
@@
   useEffect(() => {
-    const backendRole = roleFilter === 'all' ? undefined : roleFilter;
-    const backendStatus = statusFilter === 'all' ? undefined : statusFilter;
-    loadMembers({
-      keyword: searchQuery.trim() || undefined,
-      role: backendRole,
-      status: backendStatus,
-    });
+    loadMembers(buildMemberQuery());
   }, [roleFilter, searchQuery, statusFilter]);
@@
-      await loadMembers({
-        keyword: searchQuery.trim() || undefined,
-        role: roleFilter === 'all' ? undefined : roleFilter,
-        status: statusFilter === 'all' ? undefined : statusFilter,
-      });
+      await loadMembers(buildMemberQuery());
@@
-      await loadMembers({
-        keyword: searchQuery.trim() || undefined,
-        role: roleFilter === 'all' ? undefined : roleFilter,
-        status: statusFilter === 'all' ? undefined : statusFilter,
-      });
+      await loadMembers(buildMemberQuery());
@@
-      await loadMembers({
-        keyword: searchQuery.trim() || undefined,
-        role: roleFilter === 'all' ? undefined : roleFilter,
-        status: statusFilter === 'all' ? undefined : statusFilter,
-      });
+      await loadMembers(buildMemberQuery());

Also applies to: 140-144, 170-174, 202-206

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

In `@frontend/src/components/AdminMemberManage/AdminMemberManage.jsx` around lines
83 - 87, Extract the repeated construction of the member list params (currently
built inline where loadMembers is called using searchQuery.trim() || undefined,
backendRole, backendStatus) into a single helper function (e.g.,
getMemberListParams or buildMemberQueryParams) and replace all inline
occurrences (the calls around loadMembers at the shown sites and similar blocks
at the other locations) with a call to that helper; ensure the helper accepts
the current local inputs (searchQuery, backendRole, backendStatus), performs the
trim/undefined normalization, and returns the object passed into loadMembers so
all callers reuse the same logic.
frontend/src/components/attendance/SessionManage.jsx (1)

16-16: 디버그 로그는 정리하는 편이 좋습니다.

Line 16의 console.log(data)는 운영 콘솔 노이즈가 되므로 제거 권장합니다.

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

In `@frontend/src/components/attendance/SessionManage.jsx` at line 16, Remove the
debug console output by deleting the stray console.log(data) call in the
SessionManage component (SessionManage.jsx); locate the console.log(data) inside
the SessionManage function/component and remove it (or replace with a controlled
debug logger that only runs in development if you prefer), ensuring no leftover
console noise in production.
frontend/src/pages/Attendance.jsx (1)

3-3: 주석 처리된 기능 코드는 정리해주세요.

ExcusedTime import/렌더링이 주석으로 남아 있어 코드 의도가 모호합니다. 제거하거나 feature flag로 명시적으로 관리하는 편이 좋습니다.

Also applies to: 16-16

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

In `@frontend/src/pages/Attendance.jsx` at line 3, The commented-out ExcusedTime
import and its commented rendering in Attendance.jsx leave unclear intent;
either remove both commented lines entirely or restore them behind a clear
feature flag (e.g., a boolean prop or config like showExcusedTime) so the code's
intent is explicit. Locate the commented import for ExcusedTime and the
commented JSX render (around the component's render/return block), then either
delete those comments or implement a conditional render using a named flag
(e.g., showExcusedTime) and document the flag where appropriate.
frontend/src/utils/attendanceList.js (1)

15-29: 대형 주석 샘플 데이터는 제거 권장합니다.

현재 주석 블록은 실제 동작과 무관해 파일 가독성을 떨어뜨립니다. 필요하면 별도 문서나 타입 정의 파일로 옮기는 것이 좋습니다.

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

In `@frontend/src/utils/attendanceList.js` around lines 15 - 29, 현재 파일 상단에 남아있는 큰
주석형 샘플 JSON 블록(출석 항목 샘플 데이터)을 제거하거나, 필요하면 별도 문서(예: docs/ 또는 README)나 타입 정의 파일로
옮기해 파일 가독성을 회복하세요; 주석을 완전히 삭제할 수 없다면 한 줄짜리 간단한 설명으로 축소하거나 참조 링크만 남기도록 변경하세요.
frontend/src/utils/adminUserApi.js (1)

63-67: FormData를 사용할 때 Content-Type 헤더를 수동으로 설정하지 않는 것이 좋습니다.

axios와 브라우저가 FormData를 감지하면 자동으로 Content-Type: multipart/form-data; boundary=... 형태로 설정합니다. 수동으로 헤더를 지정하면 axios의 요청 처리에서 boundary 파라미터가 누락되어 서버에서 파싱 에러가 발생할 수 있습니다.

수정 제안
  const response = await api.post('/api/admin/users/upload-excel', formData, {
-   headers: {
-     'Content-Type': 'multipart/form-data',
-   },
  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/utils/adminUserApi.js` around lines 63 - 67, 현재
api.post('/api/admin/users/upload-excel', formData, { headers: { 'Content-Type':
'multipart/form-data' } })에서 FormData를 보낼 때 Content-Type을 수동으로 설정하고 있어 axios가
자동으로 생성하는 boundary가 누락될 위험이 있습니다; upload 코드를 수정해서 api.post 호출에서 headers: {
'Content-Type': ... } 옵션을 제거(또는 빈 옵션으로 대체)하여 axios/브라우저가 multipart/form-data;
boundary=... 헤더를 자동으로 설정하도록 바꿔 주세요(참조: 변수 formData 및 호출 위치
api.post('/api/admin/users/upload-excel', formData, ...)).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/components/AdminMemberApproval/AdminMemberApproval.jsx`:
- Around line 39-40: The monthly approved/rejected counters get overwritten by
loadPendingMembers after you optimistically increment them; update the logic so
server responses never blindly clobber local optimistic increments.
Specifically, in loadPendingMembers replace the direct assignments to
setMonthlyApprovedCount(data.monthlyApprovedCount || 0) and
setMonthlyRejectedCount(data.monthlyRejectedCount || 0) with an updater that
merges with prior state (e.g., use the functional setter:
setMonthlyApprovedCount(prev => data.monthlyApprovedCount != null ?
Math.max(prev, data.monthlyApprovedCount) : prev) and similarly for
setMonthlyRejectedCount) or add an optional flag/param to loadPendingMembers to
skip overwriting counts when called after approveMember/rejectMember; apply the
same change for the occurrences at the other sites (the lines referenced at
113-114 and 133-134) so optimistic increments performed in
approveMember/rejectMember are preserved.

In `@frontend/src/components/AdminMemberManage/AdminMemberManage.jsx`:
- Around line 57-66: The change modal allows double-submits; add a per-member
in-flight guard and disable the modal "변경" button while a PATCH is pending. In
the handler that sends the PATCH from the change modal (the function that reads
changeDialog and performs the API call—e.g., the confirm/submit handler for the
modal), check and set a boolean keyed by member id (reuse or extend
isPromotingById/isDeletingById to something like isChangingById[memberId])
before starting the request, return early if true, then set it back to false in
finally. Also bind that flag to disable the modal submit button so repeated
clicks won’t send duplicate PATCH requests.
- Line 1: In AdminMemberManage's useEffect that fetches/filter members (the
block around lines 68-88), guard against response race by tracking each request
and ignoring stale responses: either create an AbortController and pass its
signal to the fetch to cancel prior requests, or maintain an incrementing
requestId in a ref (e.g., requestRef.current++) and capture the id when starting
a request, then only call setMembers/setLoading for responses whose id matches
the latest; ensure you clean up (abort or mark stale) in the effect cleanup to
prevent applying old data.

In `@frontend/src/components/attendance/SessionManage.jsx`:
- Around line 14-15: The response from attendanceList() isn't validated before
being stored and later used in sessions.map, which can crash if data isn't an
array; update the code that calls attendanceList() to normalize the result
(e.g., use Array.isArray(data) to decide) and call setSessions with a safe array
(empty array fallback or filtered array of valid session objects), and also
guard the render path that uses sessions.map (in the component's render logic
where sessions.map is called) by ensuring sessions is an array before mapping
(e.g., use (sessions || []) or an Array.isArray check) so both the fetch/set
logic (attendanceList(), setSessions) and the render (sessions.map) are
protected.

In `@frontend/src/pages/Attendance.jsx`:
- Line 13: The page title in the Attendance component (<h1
className={styles.title}>) currently reads "출석조회" but the SessionSelectBox
component's label reads "출석하기", causing inconsistent UX; update the
Attendance.jsx <h1 className={styles.title}> text to "출석하기" so it matches the
label in SessionSelectBox.jsx (or alternatively change SessionSelectBox.jsx
label to "출석조회" if you prefer that terminology) — ensure both the Attendance
component title and the SessionSelectBox label use the exact same string.

In `@frontend/src/utils/attendanceList.js`:
- Around line 8-10: The catch block in attendanceList.js currently logs the
entire error object (console.error('출석 세션 불러오기 중 오류 발생: ', err)); change this to
log a safe, non-sensitive message and only non-sensitive error details (e.g.
err.message) or a sanitizedError variable, and avoid printing full
request/response metadata; for example replace the console.error call with one
that logs a user-facing message plus err.message (or a sanitized subset) and, if
you need full stack for debugging, send that to an internal debug/logger
conditioned on NODE_ENV==='development' or to a secure internal logger instead
of stdout, then rethrow or wrap the error as before.

---

Duplicate comments:
In `@frontend/src/components/AdminMemberApproval/AdminMemberApproval.jsx`:
- Around line 56-60: The filter callback in pendingMembers.filter uses
member.name/member.email/member.studentId without null/type checks which can
throw if any field is missing or not a string; update the callback (referencing
pendingMembers.filter, member.name, member.email, member.studentId,
normalizedQuery) to defensively coerce or default each field to a string before
using toLowerCase/includes (e.g., use optional chaining or fallback to '' and
String(...) then toLowerCase for name/email and String(...) for studentId) so
the includes checks never run on null/undefined/non-string values.

---

Nitpick comments:
In `@frontend/src/components/AdminExcelUpload/AdminExcelUpload.module.css`:
- Around line 190-199: The .resultBox style allows overflow but lacks a size
cap; add a max-height (e.g., max-height: 40vh or a fixed px value) to the
.resultBox rule and keep overflow: auto so very long upload results become
scrollable instead of expanding the section; adjust the exact max-height to
match the UI (or use calc() with header/footer heights) to ensure consistent
layout.
- Around line 169-176: The infinite rotation defined by the .spin class and
`@keyframes` spin must respect users' prefers-reduced-motion setting: update the
CSS to disable or remove the animation when `@media` (prefers-reduced-motion:
reduce) applies (e.g., set animation: none on .spin and avoid running keyframes)
so motion-sensitive users do not see the spinning animation; make this change
around the .spin class and `@keyframes` spin definitions to ensure the animation
is overridden for reduced-motion preferences.

In `@frontend/src/components/AdminMemberManage/AdminMemberManage.jsx`:
- Around line 83-87: Extract the repeated construction of the member list params
(currently built inline where loadMembers is called using searchQuery.trim() ||
undefined, backendRole, backendStatus) into a single helper function (e.g.,
getMemberListParams or buildMemberQueryParams) and replace all inline
occurrences (the calls around loadMembers at the shown sites and similar blocks
at the other locations) with a call to that helper; ensure the helper accepts
the current local inputs (searchQuery, backendRole, backendStatus), performs the
trim/undefined normalization, and returns the object passed into loadMembers so
all callers reuse the same logic.

In `@frontend/src/components/attendance/SessionManage.jsx`:
- Line 16: Remove the debug console output by deleting the stray
console.log(data) call in the SessionManage component (SessionManage.jsx);
locate the console.log(data) inside the SessionManage function/component and
remove it (or replace with a controlled debug logger that only runs in
development if you prefer), ensuring no leftover console noise in production.

In `@frontend/src/pages/Attendance.jsx`:
- Line 3: The commented-out ExcusedTime import and its commented rendering in
Attendance.jsx leave unclear intent; either remove both commented lines entirely
or restore them behind a clear feature flag (e.g., a boolean prop or config like
showExcusedTime) so the code's intent is explicit. Locate the commented import
for ExcusedTime and the commented JSX render (around the component's
render/return block), then either delete those comments or implement a
conditional render using a named flag (e.g., showExcusedTime) and document the
flag where appropriate.

In `@frontend/src/utils/adminUserApi.js`:
- Around line 63-67: 현재 api.post('/api/admin/users/upload-excel', formData, {
headers: { 'Content-Type': 'multipart/form-data' } })에서 FormData를 보낼 때
Content-Type을 수동으로 설정하고 있어 axios가 자동으로 생성하는 boundary가 누락될 위험이 있습니다; upload 코드를
수정해서 api.post 호출에서 headers: { 'Content-Type': ... } 옵션을 제거(또는 빈 옵션으로 대체)하여
axios/브라우저가 multipart/form-data; boundary=... 헤더를 자동으로 설정하도록 바꿔 주세요(참조: 변수
formData 및 호출 위치 api.post('/api/admin/users/upload-excel', formData, ...)).

In `@frontend/src/utils/attendanceList.js`:
- Around line 15-29: 현재 파일 상단에 남아있는 큰 주석형 샘플 JSON 블록(출석 항목 샘플 데이터)을 제거하거나, 필요하면
별도 문서(예: docs/ 또는 README)나 타입 정의 파일로 옮기해 파일 가독성을 회복하세요; 주석을 완전히 삭제할 수 없다면 한 줄짜리
간단한 설명으로 축소하거나 참조 링크만 남기도록 변경하세요.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8cddcaf and 130a6d7.

📒 Files selected for processing (15)
  • frontend/src/components/AdminExcelUpload/AdminExcelUpload.jsx
  • frontend/src/components/AdminExcelUpload/AdminExcelUpload.module.css
  • frontend/src/components/AdminHome/AdminHeader.jsx
  • frontend/src/components/AdminHome/AdminHeader.module.css
  • frontend/src/components/AdminHome/AdminSidebar.jsx
  • frontend/src/components/AdminMemberApproval/AdminMemberApproval.jsx
  • frontend/src/components/AdminMemberManage/AdminMemberManage.jsx
  • frontend/src/components/AdminMemberManage/AdminMemberManage.module.css
  • frontend/src/components/attendance/SessionManage.jsx
  • frontend/src/pages/AdminHome.jsx
  • frontend/src/pages/Attendance.jsx
  • frontend/src/utils/adminHomeData.js
  • frontend/src/utils/adminMembersData.js
  • frontend/src/utils/adminUserApi.js
  • frontend/src/utils/attendanceList.js
🚧 Files skipped from review as they are similar to previous changes (6)
  • frontend/src/components/AdminHome/AdminHeader.module.css
  • frontend/src/components/AdminHome/AdminHeader.jsx
  • frontend/src/components/AdminExcelUpload/AdminExcelUpload.jsx
  • frontend/src/components/AdminMemberManage/AdminMemberManage.module.css
  • frontend/src/pages/AdminHome.jsx
  • frontend/src/utils/adminMembersData.js

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

🧹 Nitpick comments (4)
frontend/src/components/attendance/SessionManage.jsx (1)

57-57: 하드코딩된 "30분" 값 확인 필요

출석 가능 시간이 고정값인지, 향후 API에서 동적으로 제공될 예정인지 확인이 필요합니다. 변경 가능성이 있다면 상수나 설정으로 분리하는 것을 권장합니다.

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

In `@frontend/src/components/attendance/SessionManage.jsx` at line 57, The table
cell in SessionManage.jsx currently hardcodes "30분"; replace this literal with a
configurable value (e.g., use a constant like SESSION_DURATION, a prop such as
sessionDuration, or the API-provided field from the session object) and update
rendering in the component (SessionManage) to read that variable; add a default
fallback (e.g., 30) and move any unit formatting ("분") into the render logic so
the duration source can be switched to settings or API without changing markup.
frontend/src/components/AdminMemberManage/AdminMemberManage.jsx (3)

152-163: 유효성 검사 실패 시 isChangingById 플래그가 해제되지 않습니다.

try 블록 내 조기 return(라인 156, 162)은 finally 블록을 우회하지 않지만, return 전에 finally가 실행되므로 실제로는 문제가 없습니다. 그러나 가독성과 명확한 의도 전달을 위해 유효성 검사를 try 블록 이전으로 이동하는 것이 좋습니다.

♻️ 유효성 검사를 try 블록 이전으로 이동
   const confirmChangeDialog = async () => {
     const { type, member, value } = changeDialog;
     if (!member) return;

     if (isChangingById[member.id]) {
       return;
     }

+    // 유효성 검사를 먼저 수행
+    if (type === 'role' && !ROLE_OPTIONS.includes(value)) {
+      window.alert('유효하지 않은 권한입니다.');
+      return;
+    }
+    if (type !== 'role' && !STATUS_OPTIONS.includes(value)) {
+      window.alert('유효하지 않은 상태입니다.');
+      return;
+    }
+
     setIsChangingById((prev) => ({
       ...prev,
       [member.id]: true,
     }));

     try {
       if (type === 'role') {
-        if (!ROLE_OPTIONS.includes(value)) {
-          window.alert('유효하지 않은 권한입니다.');
-          return;
-        }
         await changeAdminMemberRole({ userId: member.id, role: value });
       } else {
-        if (!STATUS_OPTIONS.includes(value)) {
-          window.alert('유효하지 않은 상태입니다.');
-          return;
-        }
         await changeAdminMemberStatus({ userId: member.id, status: value });
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/AdminMemberManage/AdminMemberManage.jsx` around lines
152 - 163, Move the ROLE_OPTIONS/STATUS_OPTIONS validation logic out of the try
block and perform it before setting the isChangingById flag so early returns
don't risk confusing intent; specifically, validate value against ROLE_OPTIONS
when type === 'role' and against STATUS_OPTIONS when type !== 'role' (and return
on invalid) before entering the try where you call changeAdminMemberRole or
other update functions (e.g., changeAdminMemberRole({ userId: member.id, role:
value })), ensuring isChangingById is only set after validation and still
cleared in finally.

255-260: 검색 입력에 디바운스(debounce) 적용을 권장합니다.

현재 검색 입력 시 매 키 입력마다 useEffect가 트리거되어 API 호출이 발생합니다. 빠르게 타이핑할 경우 불필요한 요청이 다수 발생할 수 있습니다.

♻️ 디바운스 적용 예시
-import { useEffect, useMemo, useRef, useState } from 'react';
+import { useEffect, useMemo, useRef, useState, useCallback } from 'react';
@@
 const AdminMemberManage = () => {
   const [searchQuery, setSearchQuery] = useState('');
+  const [debouncedSearch, setDebouncedSearch] = useState('');
+
+  // 검색어 디바운스 처리
+  useEffect(() => {
+    const timer = setTimeout(() => {
+      setDebouncedSearch(searchQuery);
+    }, 300);
+    return () => clearTimeout(timer);
+  }, [searchQuery]);
@@
   useEffect(() => {
     const requestId = ++latestRequestIdRef.current;
     const backendRole = roleFilter === 'all' ? undefined : roleFilter;
     const backendStatus = statusFilter === 'all' ? undefined : statusFilter;
     loadMembers({
-      keyword: searchQuery.trim() || undefined,
+      keyword: debouncedSearch.trim() || undefined,
       role: backendRole,
       status: backendStatus,
       requestId,
     });
     // ...
-  }, [roleFilter, searchQuery, statusFilter]);
+  }, [roleFilter, debouncedSearch, statusFilter]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/AdminMemberManage/AdminMemberManage.jsx` around lines
255 - 260, The search input currently updates searchQuery on every keystroke
causing the useEffect that calls the API to run too often; introduce a debounced
value (e.g., debouncedSearchQuery) in the AdminMemberManage component and use
that in the API useEffect instead of searchQuery. Implement debouncing by
deriving debouncedSearchQuery from searchQuery with a timeout (or use
lodash.debounce / a custom useDebounce hook), update the input handler to still
call setSearchQuery(event.target.value), change the effect that fetches members
to depend on debouncedSearchQuery, and ensure you clear the timeout/cleanup in
the effect to avoid memory leaks.

377-387: 페이지네이션 UI가 비활성화 상태입니다.

현재 페이지네이션 버튼이 disabled 상태로 되어 있습니다. 향후 대용량 회원 목록 처리를 위해 서버 사이드 페이지네이션 구현이 필요할 수 있습니다.

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

In `@frontend/src/components/AdminMemberManage/AdminMemberManage.jsx` around lines
377 - 387, The pagination buttons in AdminMemberManage are hard-disabled; remove
the static disabled attributes and implement client-side pagination state (add
currentPage and pageSize state via setCurrentPage/setPageSize) and compute
displayedMembers by slicing filteredMembers (e.g., displayedMembers =
filteredMembers.slice(currentPage*pageSize, (currentPage+1)*pageSize)); add
handlers handlePrevPage and handleNextPage bound to the buttons in the element
with className styles.paging and keep the buttons visually disabled only when at
bounds (currentPage === 0 or (currentPage+1)*pageSize >= filteredMembers.length)
to prevent overflow; ensure you update any rendering to use displayedMembers
instead of filteredMembers and update the footer count or show current page info
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/components/attendance/SessionManage.jsx`:
- Around line 54-59: The table renders raw dates using new
Date(s.createdAt).toLocaleDateString() and new
Date(s.checkedAt).toLocaleTimeString(), which shows "Invalid Date" when values
are null/undefined/invalid; update the rendering in SessionManage.jsx (the <tr
key={s.attendanceId}> row) to defensively check/validate s.createdAt and
s.checkedAt before formatting (e.g., use a small helper like isValidDate(value)
or Date.parse check) and render a fallback such as "-" or an empty string when
the value is missing or invalid so users never see "Invalid Date".

---

Nitpick comments:
In `@frontend/src/components/AdminMemberManage/AdminMemberManage.jsx`:
- Around line 152-163: Move the ROLE_OPTIONS/STATUS_OPTIONS validation logic out
of the try block and perform it before setting the isChangingById flag so early
returns don't risk confusing intent; specifically, validate value against
ROLE_OPTIONS when type === 'role' and against STATUS_OPTIONS when type !==
'role' (and return on invalid) before entering the try where you call
changeAdminMemberRole or other update functions (e.g., changeAdminMemberRole({
userId: member.id, role: value })), ensuring isChangingById is only set after
validation and still cleared in finally.
- Around line 255-260: The search input currently updates searchQuery on every
keystroke causing the useEffect that calls the API to run too often; introduce a
debounced value (e.g., debouncedSearchQuery) in the AdminMemberManage component
and use that in the API useEffect instead of searchQuery. Implement debouncing
by deriving debouncedSearchQuery from searchQuery with a timeout (or use
lodash.debounce / a custom useDebounce hook), update the input handler to still
call setSearchQuery(event.target.value), change the effect that fetches members
to depend on debouncedSearchQuery, and ensure you clear the timeout/cleanup in
the effect to avoid memory leaks.
- Around line 377-387: The pagination buttons in AdminMemberManage are
hard-disabled; remove the static disabled attributes and implement client-side
pagination state (add currentPage and pageSize state via
setCurrentPage/setPageSize) and compute displayedMembers by slicing
filteredMembers (e.g., displayedMembers =
filteredMembers.slice(currentPage*pageSize, (currentPage+1)*pageSize)); add
handlers handlePrevPage and handleNextPage bound to the buttons in the element
with className styles.paging and keep the buttons visually disabled only when at
bounds (currentPage === 0 or (currentPage+1)*pageSize >= filteredMembers.length)
to prevent overflow; ensure you update any rendering to use displayedMembers
instead of filteredMembers and update the footer count or show current page info
accordingly.

In `@frontend/src/components/attendance/SessionManage.jsx`:
- Line 57: The table cell in SessionManage.jsx currently hardcodes "30분";
replace this literal with a configurable value (e.g., use a constant like
SESSION_DURATION, a prop such as sessionDuration, or the API-provided field from
the session object) and update rendering in the component (SessionManage) to
read that variable; add a default fallback (e.g., 30) and move any unit
formatting ("분") into the render logic so the duration source can be switched to
settings or API without changing markup.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 130a6d7 and af7df73.

📒 Files selected for processing (5)
  • frontend/src/components/AdminMemberApproval/AdminMemberApproval.jsx
  • frontend/src/components/AdminMemberManage/AdminMemberManage.jsx
  • frontend/src/components/attendance/SessionManage.jsx
  • frontend/src/pages/Attendance.jsx
  • frontend/src/utils/attendanceList.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/src/components/AdminMemberApproval/AdminMemberApproval.jsx
  • frontend/src/pages/Attendance.jsx

@discipline24 discipline24 merged commit 5bacf07 into main Feb 26, 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.

⚙️ [기능추가][관리자] 관리자 페이지 회원 페이지 구현

2 participants