Skip to content

Feature#335 관리자 대시보드 페이지 추가#362

Merged
kimgho merged 8 commits into
mainfrom
feature#335
Aug 23, 2025
Merged

Feature#335 관리자 대시보드 페이지 추가#362
kimgho merged 8 commits into
mainfrom
feature#335

Conversation

@kimgho
Copy link
Copy Markdown
Contributor

@kimgho kimgho commented Aug 23, 2025

✅ Linked Issue

🔍 What I did

  • 관리자 대시보드 페이지 추가
    • 학생 정보 테이블을 별도의 페이지로 분리
    • 검색 및 정렬을 위한 공용 훅 추가

🚧 TODO (if any)

  • [ ]도시락 신청 정보와 차트 데이터는 어떻게 할 지 생각해야함

Summary by CodeRabbit

  • New Features

    • Added a dedicated Students page with search (name/phone), sorting, pagination, and Excel download.
    • Added a “학생 정보” navigation item.
  • Improvements

    • Dashboard statistics now update from live API data.
    • Pagination logic centralized for clearer navigation.
  • Changes

    • Removed embedded student list from the Dashboard; moved to Students page.
    • Replaced header modal login with a full Login page and updated routing.
  • Chores

    • Added shared hooks and routing updates to support these features.

@vercel
Copy link
Copy Markdown

vercel Bot commented Aug 23, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
mosu-client Ready Ready Preview Comment Aug 23, 2025 5:21pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Aug 23, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Adds typed API clients and React Query hooks for dashboard info and student list, introduces StudentsPage with search/sort/pagination, replaces pagination range logic with a new usePagination hook, wires dashboard stats to backend, updates routes and navigation, removes StudentTable and LoginModal, and adds common hooks barrel exports.

Changes

Cohort / File(s) Summary
API: Dashboard & Students
mosu-admin/src/api/dashboard/getDashboardInfo.ts, mosu-admin/src/api/dashboard/getStudentList.ts
New typed API modules and React Query hooks for dashboard counts and paginated student listing with filters.
Hooks: Common utilities
mosu-admin/src/hooks/common/usePagination.tsx, mosu-admin/src/hooks/common/useSearchByName.tsx, mosu-admin/src/hooks/common/useSearchByPhone.tsx, mosu-admin/src/hooks/common/useSortOrder.tsx, mosu-admin/src/hooks/common/index.ts
Adds pagination, search-by-name/phone, and sort-order hooks; creates a barrel export for common hooks.
Components: Pagination, Dashboard stats, Header changes
mosu-admin/src/components/CommonPagination.tsx, mosu-admin/src/components/dashboard/StatSection.tsx, mosu-admin/src/layout/ui/MainHeader.tsx
CommonPagination switched to usePagination; StatSection now reads dashboard counts via API hook; MainHeader login modal/controls removed.
Pages: Students & Dashboard update
mosu-admin/src/pages/StudentsPage.tsx, mosu-admin/src/pages/DashboardPage.tsx
New StudentsPage with search/sort/pagination; DashboardPage no longer renders StudentTable.
Routing & Navigation
mosu-admin/src/constants/routes.ts, mosu-admin/src/routes.tsx, mosu-admin/src/layout/navItems.tsx
Adds ROUTES.STUDENTS, registers StudentsPage route, and adds "학생 정보" nav item.
Removal
mosu-admin/src/components/dashboard/StudentTable.tsx, mosu-admin/src/components/signin/LoginModal.tsx
Deletes StudentTable component and LoginModal component (files removed).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Admin as Admin User
  participant UI as StudentsPage
  participant Hooks as Search/Sort/Pagination Hooks
  participant Query as React Query
  participant API as /admin/students

  Admin->>UI: Load /students
  UI->>Hooks: init filters, sort, page
  UI->>Query: useQuery(["student", params], getStudentList)
  Query->>API: GET /admin/students?name&phone&order&page&size
  API-->>Query: 200 OK (paginated students)
  Query-->>UI: data { content, totalPages, ... }
  UI-->>Admin: Render table + pagination

  Admin->>UI: Change filters/sort/page
  UI->>Hooks: update state
  UI->>Query: refetch with new params
  API-->>Query: 200 OK
  Query-->>UI: updated data
  UI-->>Admin: Updated table
Loading
sequenceDiagram
  autonumber
  actor Admin as Admin User
  participant Dash as StatSection
  participant Query as React Query
  participant API as /admin/dashboard

  Admin->>Dash: Open Dashboard
  Dash->>Query: useQuery(["dashboard"], getDashboardInfo)
  Query->>API: GET /admin/dashboard
  API-->>Query: 200 OK { userCounts, refundAbortedCounts, applicationCounts }
  Query-->>Dash: data
  Dash-->>Admin: Render stats with counts
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Assessment against linked issues

Objective Addressed Explanation
관리자 대시보드 페이지 추가 (#335)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Add Students page and logic (mosu-admin/src/pages/StudentsPage.tsx) Students listing page extends beyond the dashboard addition request.
New student list API and hook (mosu-admin/src/api/dashboard/getStudentList.ts) Student listing API client is not required to add the dashboard page.
Add STUDENTS route & nav item (mosu-admin/src/constants/routes.ts, mosu-admin/src/routes.tsx, mosu-admin/src/layout/navItems.tsx) New route and navigation for Students are not strictly part of the dashboard page objective.

Possibly related PRs

Suggested labels

기능 구현

Suggested reviewers

  • toothlessdev

Poem

"A rabbit hopped through code today,
I counted stats in bright array.
Pages turn with a tiny cheer,
Students list — I'm almost near.
Hopping routes and hooks in tune — 🥕"

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5119d54 and 17152b2.

📒 Files selected for processing (4)
  • mosu-admin/src/components/signin/LoginModal.tsx (0 hunks)
  • mosu-admin/src/layout/ui/MainHeader.tsx (0 hunks)
  • mosu-admin/src/pages/LoginPage.tsx (1 hunks)
  • mosu-admin/src/routes.tsx (2 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature#335

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Aug 23, 2025

Messages
📖 ✅ PR 제목에 이슈 번호가 포함되어 있습니다.
📖 ✅ PR에 Reviewers가 지정되어 있습니다.
📖 ✅ PR에 라벨이 지정되어 있습니다.
📖 ✅ PR에 Assignees가 지정되어 있습니다.
📖 ✅ package.json에 변경사항이 없습니다.
📖 ✅ 브랜치 이름 'feature#335'이 컨벤션을 따릅니다.
📖 ✅ TypeScript 컴파일이 성공적으로 완료되었습니다.
📖 ✅ ESLint 검사 결과 문제가 없습니다.

📝 추가 및 변경된 파일

총 16개 파일 변경

└── 📂 mosu-admin/
    └── 📂 src/
        ├── 📂 components/
        │   ├── ⚛️ CommonPagination.tsx
        │   └── 📂 dashboard/
        │       └── ⚛️ StatSection.tsx
        ├── 📂 constants/
        │   └── 📘 routes.ts
        ├── 📂 layout/
        │   ├── ⚛️ navItems.tsx
        │   └── 📂 ui/
        │       └── ⚛️ MainHeader.tsx
        ├── 📂 pages/
        │   ├── ⚛️ DashboardPage.tsx
        │   ├── ⚛️ LoginPage.tsx
        │   └── ⚛️ StudentsPage.tsx
        ├── ⚛️ routes.tsx
        ├── 📂 api/
        │   └── 📂 dashboard/
        │       ├── 📘 getDashboardInfo.ts
        │       └── 📘 getStudentList.ts
        └── 📂 hooks/
            └── 📂 common/
                ├── 📘 index.ts
                ├── ⚛️ usePagination.tsx
                ├── ⚛️ useSearchByName.tsx
                ├── ⚛️ useSearchByPhone.tsx
                └── ⚛️ useSortOrder.tsx

Generated by 🚫 dangerJS against 17152b2

@github-actions
Copy link
Copy Markdown

✅ Mosu Admin 빌드가 성공적으로 완료되었습니다!

📋 빌드 결과

  • ✅ DangerJS 검사 통과
  • ✅ React 앱 빌드 성공

🚀 다음 단계

코드 리뷰 후 머지하면 프로덕션에 배포됩니다.

Copy link
Copy Markdown

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

🧹 Nitpick comments (27)
mosu-admin/src/hooks/common/useSearchByName.tsx (1)

7-16: Trim input and stabilize handlers with useCallback

To avoid sending trailing/leading spaces to the API and to keep handler identities stable when passing them down, trim on apply and wrap the handlers with useCallback. This also makes hasNameSearch reflect meaningful text.

Apply:

-import { useState } from "react";
+import { useCallback, useState } from "react";

 export const useSearchByName = () => {
   const [searchName, setSearchName] = useState("");
   const [appliedSearchName, setAppliedSearchName] = useState("");

-  const handleNameSearch = () => {
-      setAppliedSearchName(searchName);
-  };
+  const handleNameSearch = useCallback(() => {
+      setAppliedSearchName(searchName.trim());
+  }, [searchName]);
 
-  const handleNameReset = () => {
-      setSearchName("");
-      setAppliedSearchName("");
-  };
+  const handleNameReset = useCallback(() => {
+      setSearchName("");
+      setAppliedSearchName("");
+  }, []);
 
-  const hasNameSearch = !!appliedSearchName;
+  const hasNameSearch = appliedSearchName.trim().length > 0;

Also applies to: 1-1

mosu-admin/src/hooks/common/useSearchByPhone.tsx (1)

7-16: Normalize phone input and stabilize handlers

Phone searches commonly require digit-only values. Normalize by stripping non-digits on apply, trim input, and memoize handlers with useCallback to avoid unnecessary re-renders.

Apply:

-import { useState } from "react";
+import { useCallback, useState } from "react";

 export const useSearchByPhone = () => {
   const [searchPhone, setSearchPhone] = useState("");
   const [appliedSearchPhone, setAppliedSearchPhone] = useState("");

-  const handlePhoneSearch = () => {
-      setAppliedSearchPhone(searchPhone);
-  };
+  const handlePhoneSearch = useCallback(() => {
+      const normalized = searchPhone.trim().replace(/\D/g, "");
+      setAppliedSearchPhone(normalized);
+  }, [searchPhone]);
 
-  const handlePhoneReset = () => {
-      setSearchPhone("");
-      setAppliedSearchPhone("");
-  };
+  const handlePhoneReset = useCallback(() => {
+      setSearchPhone("");
+      setAppliedSearchPhone("");
+  }, []);
 
-  const hasPhoneSearch = !!appliedSearchPhone;
+  const hasPhoneSearch = appliedSearchPhone.length > 0;

Also applies to: 1-1

mosu-admin/src/hooks/common/usePagination.tsx (2)

8-41: Guard against out-of-range currentPage and use a clamped value

If upstream passes 0 or a value > totalPages (e.g., after filter changes), visiblePages and next/prev flags can be inconsistent. Clamp currentPage to [1, totalPages] once and derive everything from it.

Apply:

 export const usePagination = ({ currentPage, totalPages }: UsePaginationProps) => {
-    const visiblePages = useMemo(() => {
+    const safeCurrent = Math.min(Math.max(currentPage, 1), Math.max(totalPages, 1));
+    const visiblePages = useMemo(() => {
         if (totalPages <= 1) return [];
 
-        const delta = 2;
+        const delta = 2;
         const range = [];
         const rangeWithDots: (number | string)[] = [];
 
-        for (
-            let i = Math.max(2, currentPage - delta);
-            i <= Math.min(totalPages - 1, currentPage + delta);
+        for (
+            let i = Math.max(2, safeCurrent - delta);
+            i <= Math.min(totalPages - 1, safeCurrent + delta);
             i++
         ) {
             range.push(i);
         }
 
-        if (currentPage - delta > 2) {
+        if (safeCurrent - delta > 2) {
             rangeWithDots.push(1, "...");
         } else {
             rangeWithDots.push(1);
         }
 
         rangeWithDots.push(...range);
 
-        if (currentPage + delta < totalPages - 1) {
+        if (safeCurrent + delta < totalPages - 1) {
             rangeWithDots.push("...", totalPages);
         } else {
             if (totalPages > 1) {
                 rangeWithDots.push(totalPages);
             }
         }
 
         return rangeWithDots;
-    }, [currentPage, totalPages]);
+    }, [safeCurrent, totalPages]);
 
-    const canGoPrevious = currentPage > 1;
-    const canGoNext = currentPage < totalPages;
+    const canGoPrevious = safeCurrent > 1;
+    const canGoNext = safeCurrent < totalPages;
 
     return {
         visiblePages,
         canGoPrevious,
         canGoNext,
     };
 };

3-6: Optional: make delta configurable

If you anticipate different pagination densities across pages/components, accept an optional delta in props (default 2).

Alternative:

-interface UsePaginationProps {
-    currentPage: number;
-    totalPages: number;
-}
+interface UsePaginationProps {
+    currentPage: number;
+    totalPages: number;
+    delta?: number;
+}
 
-export const usePagination = ({ currentPage, totalPages }: UsePaginationProps) => {
+export const usePagination = ({ currentPage, totalPages, delta = 2 }: UsePaginationProps) => {
-    const delta = 2;

Also applies to: 12-15

mosu-admin/src/routes.tsx (2)

11-11: Import is fine — consider lazy-loading StudentsPage

StudentsPage is a secondary route. Lazy-loading can cut initial bundle size.

Apply minimally:

-import { StudentsPage } from "@/pages/StudentsPage";
+import { Suspense, lazy } from "react";
+const StudentsPage = lazy(() => import("@/pages/StudentsPage"));

And wrap the element in Suspense where used (see next comment).


20-20: Route wiring LGTM; optionally wrap with Suspense for lazy import

If you adopt lazy-loading, wrap the element with Suspense.

Suggested route element:

-<Route path={ROUTES.STUDENTS} element={<StudentsPage />} />
+<Route
+  path={ROUTES.STUDENTS}
+  element={
+    <Suspense fallback={null}>
+      <StudentsPage />
+    </Suspense>
+  }
/>
mosu-admin/src/hooks/common/index.ts (1)

1-5: Barrel export LGTM; also re-export SortOrder type for ergonomics

This avoids deep imports for types when consuming the hook.

 export { usePagination } from "./usePagination";
 export { useSearchByName } from "./useSearchByName";
 export { useSearchByPhone } from "./useSearchByPhone";
 export { useSortOrder } from "./useSortOrder";
 export { useOverlay } from "./useOverlay";
+export type { SortOrder } from "./useSortOrder";
mosu-admin/src/components/CommonPagination.tsx (3)

4-4: Prefer importing hooks from the common barrel for consistency

Keeps import style uniform across the codebase and eases refactors.

-import { usePagination } from "@/hooks/common/usePagination";
+import { usePagination } from "@/hooks/common";

26-33: Add accessible labels to navigation arrows

Improves screen-reader usability without changing behavior.

             <Button
                 variant="outline"
                 size="sm"
                 onClick={() => onPageChange(currentPage - 1)}
-                disabled={!canGoPrevious}
+                disabled={!canGoPrevious}
+                aria-label="이전 페이지"
             >
                 <ChevronLeft className="h-4 w-4" />
             </Button>
@@
             <Button
                 variant="outline"
                 size="sm"
                 onClick={() => onPageChange(currentPage + 1)}
-                disabled={!canGoNext}
+                disabled={!canGoNext}
+                aria-label="다음 페이지"
             >
                 <ChevronRight className="h-4 w-4" />
             </Button>

Also applies to: 51-56


35-49: Stabilize keys and improve a11y on page items

  • Stable keys reduce re-renders when the window shifts.
  • aria-current communicates the current page to assistive tech.
  • Optional: avoid calling onPageChange via boolean short-circuit.
-            {visiblePages.map((page, index) => (
+            {visiblePages.map((page, index) => (
                 <Button
-                    key={index}
+                    key={typeof page === "number" ? `page-${page}` : `ellipsis-${index}`}
                     variant={page === currentPage ? "default" : "outline"}
                     size="sm"
-                    onClick={() => typeof page === "number" && onPageChange(page)}
+                    onClick={typeof page === "number" ? () => onPageChange(page) : undefined}
                     disabled={page === "..."}
                     className={cn(
                         "min-w-8",
                         page === "..." && "cursor-default hover:bg-transparent",
                     )}
+                    aria-current={page === currentPage ? "page" : undefined}
+                    aria-label={typeof page === "number" ? `페이지 ${page}` : "더 보기"}
                 >
                     {page}
                 </Button>
             ))}
mosu-admin/src/hooks/common/useSortOrder.tsx (2)

8-10: Narrow input type and guard at runtime to prevent invalid sort values

Accepting a plain string risks leaking invalid values downstream. Constrain to SortOrder and add a runtime guard (useful when values come from UI components).

-    const handleSortChange = (value: string) => {
-        setSortOrder(value as SortOrder);
-    };
+    const handleSortChange = (value: unknown) => {
+        if (value === "asc" || value === "desc") {
+            setSortOrder(value);
+        }
+    };

1-1: Memoize handlers to reduce re-renders in child components

Small optimization; keeps handler identity stable when passed to controlled inputs.

-import { useState } from "react";
+import { useCallback, useState } from "react";
@@
-    const handleSortReset = () => {
-        setSortOrder(undefined);
-    };
+    const handleSortReset = useCallback(() => {
+        setSortOrder(undefined);
+    }, []);

Also applies to: 12-14

mosu-admin/src/components/dashboard/StatSection.tsx (2)

15-15: Expose loading/error state from the query (optional UI polish)

Using isLoading/isError enables showing a skeleton or fallback rather than zeroed counts.

-    const { data: dashboardCounts } = useGetDashboardInfo();
+    const { data: dashboardCounts, isLoading, isError } = useGetDashboardInfo();

You can pass isLoading down to StatCard or render a lightweight skeleton locally until data arrives.


62-62: Avoid hard-coded “기준” date on the chart subtitle

If the chart remains static for now, prefer a dynamic “as of” date to prevent stale UI text.

-                        subtitle="25.06.18 기준"
+                        subtitle={`${new Date().toLocaleDateString("ko-KR")} 기준`}
mosu-admin/src/api/dashboard/getDashboardInfo.ts (3)

10-10: Add explicit return type to the API function

Being explicit helps with inference across call sites and prevents accidental return-shape drift.

Apply:

-export const getDashboardInfo = async () => {
+export const getDashboardInfo = async (): Promise<GetDashboardInfoResponseBody> => {

15-18: Preserve error context when rethrowing

Catching and throwing a new blank Error() drops the original message/stack. Just rethrow or wrap with a message (optionally using cause if your TS lib target supports it).

Apply one of:

-    } catch (error) {
-        console.error(error);
-        throw new Error();
-    }
+    } catch (error) {
+        if (process.env.NODE_ENV !== "production") console.error(error);
+        throw error as Error;
+    }

or

-    } catch (error) {
-        console.error(error);
-        throw new Error();
-    }
+    } catch (error) {
+        throw new Error("Failed to fetch dashboard info", { cause: error as Error });
+    }

21-25: Consider query options tuned for dashboards (freshness/refresh)

Dashboards usually benefit from periodic refresh and less jitter. Optionally set staleTime and a light refetchInterval.

Example:

 export const useGetDashboardInfo = () => {
   return useQuery({
     queryKey: ["dashboard"],
     queryFn: getDashboardInfo,
+    staleTime: 30_000,
+    refetchOnWindowFocus: false,
+    refetchInterval: 30_000,
   });
 };
mosu-admin/src/pages/StudentsPage.tsx (6)

6-6: Memoize filter object to stabilize queryKey and avoid unnecessary churn

getSearchFilter() creates a new object each render. React Query hashes keys, so this is mostly fine, but memoizing makes identity stable and reduces noise.

Apply:

-import { useState } from "react";
+import { useMemo, useState } from "react";
-    const getSearchFilter = () => ({
-        ...(appliedSearchName && { name: appliedSearchName }),
-        ...(appliedSearchPhone && { phone: appliedSearchPhone }),
-        ...(sortOrder && { order: sortOrder }),
-    });
+    const searchFilter = useMemo(
+        () => ({
+            ...(appliedSearchName && { name: appliedSearchName }),
+            ...(appliedSearchPhone && { phone: appliedSearchPhone }),
+            ...(sortOrder && { order: sortOrder }),
+        }),
+        [appliedSearchName, appliedSearchPhone, sortOrder],
+    );
-    } = useGetStudentList({
-        filter: getSearchFilter(),
+    } = useGetStudentList({
+        filter: searchFilter,
         pageable: {
             page: currentPage - 1,
             size: 30,
         },
     });

62-65: Narrow the sort type to enforce "asc" | "desc" at the boundary

The handler currently accepts string. Narrowing here prevents unexpected values flowing into the API.

Apply:

-    const handleSortChangeWithPageReset = (value: string) => {
+    const handleSortChangeWithPageReset = (value: "asc" | "desc") => {
         handleSortChange(value);
         setCurrentPage(1);
     };
-                                <Select
-                                    value={sortOrder}
-                                    onValueChange={handleSortChangeWithPageReset}
-                                >
+                                <Select
+                                    value={sortOrder}
+                                    onValueChange={(v) => handleSortChangeWithPageReset(v as "asc" | "desc")}
+                                >

Also applies to: 113-118


67-71: Tighten keyboard event typing for inputs

Add the element generic to improve type safety.

Apply:

-    const handleKeyPress = (e: React.KeyboardEvent) => {
+    const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
         if (e.key === "Enter") {
             handleSearch();
         }
     };

159-189: Empty-state row and stable keys for table items

  • Show an empty row when content.length === 0 for clearer UX.
  • Avoid index as key; prefer a stable composite (e.g., phoneNumber + birthDate) to reduce reconciliation glitches when paginating.

Apply:

-                                <TableBody>
-                                    {studentData?.content?.map((student, index) => (
-                                        <TableRow key={index}>
+                                <TableBody>
+                                    {studentData.content.length === 0 ? (
+                                        <TableRow>
+                                            <TableCell colSpan={8} className="py-8 text-center text-gray-500">
+                                                조회된 학생이 없습니다.
+                                            </TableCell>
+                                        </TableRow>
+                                    ) : (
+                                        studentData.content.map((student) => (
+                                            <TableRow key={`${student.phoneNumber}-${student.birthDate}`}>
                                                 <TableCell className="border-r">
                                                     {student.name}
                                                 </TableCell>
                                                 <TableCell className="border-r">
                                                     {student.birthDate}
                                                 </TableCell>
                                                 <TableCell className="border-r">
                                                     {student.phoneNumber}
                                                 </TableCell>
                                                 <TableCell className="border-r">
                                                     {student.gender}
                                                 </TableCell>
                                                 <TableCell className="border-r">
                                                     {student.educationLevel}
                                                 </TableCell>
                                                 <TableCell className="border-r">
                                                     {student.schoolName}
                                                 </TableCell>
                                                 <TableCell className="border-r">
                                                     {student.grade}
                                                 </TableCell>
                                                 <TableCell className="border-r">
                                                     {student.examCount}
                                                 </TableCell>
-                                        </TableRow>
-                                    ))}
+                                            </TableRow>
+                                        ))
+                                    )}
                                 </TableBody>

154-156: Column header vs. field mismatch: verify the domain term

Header text says “시청 신청 수” but the field used is examCount. If this column represents “신청 횟수” (applications) or “도시락 신청 수”, the backing property name should align (e.g., applicationCount). If it truly is exam-related, adjust the header.

Would you like me to propagate a consistent name across API types and UI?

Also applies to: 183-185


73-76: Optional: add a retry path and avoid full-page “로딩 중...” flash on pagination

  • For paginated tables, consider keeping previous data during page transitions (see hook suggestion below) and rendering a lightweight inline spinner instead of replacing the whole page.
  • You can also expose refetch from the hook and provide a retry button in the error state.

If you want, I can wire a small inline spinner and a Retry button pattern consistent with other pages.

mosu-admin/src/api/dashboard/getStudentList.ts (4)

61-61: Add explicit return type

Clarifies the contract and helps callers.

Apply:

-export const getStudentList = async (params?: GetStudentListParams) => {
+export const getStudentList = async (params?: GetStudentListParams): Promise<GetStudentListResponseBody> => {

80-83: Preserve error details on failure

Same rationale as the dashboard API. Don’t throw a blank Error().

Apply:

-    } catch (error) {
-        console.error(error);
-        throw new Error();
-    }
+    } catch (error) {
+        if (process.env.NODE_ENV !== "production") console.error(error);
+        throw error as Error;
+    }

29-43: Avoid duplicating pagination/sort meta types across modules

Sort/Pageable here look similar to those used in src/api/application/getApplications.ts (where Pageable.sort: SortInfo). Divergent duplicates drift quickly.

Consider extracting shared types into src/api/types/pagination.ts (e.g., SortInfo, Pageable) and reusing them here and elsewhere. I can open a follow-up PR to consolidate if you’d like.

Also applies to: 35-42


86-91: Pagination UX: keep previous data while fetching the next page

Reduces flicker when changing pages/sorting.

Apply:

 export const useGetStudentList = (params?: GetStudentListParams) => {
     return useQuery({
         queryKey: ["student", params],
         queryFn: () => getStudentList(params),
+        // v4/v5-safe way to emulate keepPreviousData
+        placeholderData: (prev) => prev,
+        staleTime: 15_000,
     });
 };
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a49c231 and 5119d54.

📒 Files selected for processing (15)
  • mosu-admin/src/api/dashboard/getDashboardInfo.ts (1 hunks)
  • mosu-admin/src/api/dashboard/getStudentList.ts (1 hunks)
  • mosu-admin/src/components/CommonPagination.tsx (4 hunks)
  • mosu-admin/src/components/dashboard/StatSection.tsx (2 hunks)
  • mosu-admin/src/components/dashboard/StudentTable.tsx (0 hunks)
  • mosu-admin/src/constants/routes.ts (1 hunks)
  • mosu-admin/src/hooks/common/index.ts (1 hunks)
  • mosu-admin/src/hooks/common/usePagination.tsx (1 hunks)
  • mosu-admin/src/hooks/common/useSearchByName.tsx (1 hunks)
  • mosu-admin/src/hooks/common/useSearchByPhone.tsx (1 hunks)
  • mosu-admin/src/hooks/common/useSortOrder.tsx (1 hunks)
  • mosu-admin/src/layout/navItems.tsx (1 hunks)
  • mosu-admin/src/pages/DashboardPage.tsx (0 hunks)
  • mosu-admin/src/pages/StudentsPage.tsx (1 hunks)
  • mosu-admin/src/routes.tsx (2 hunks)
💤 Files with no reviewable changes (2)
  • mosu-admin/src/components/dashboard/StudentTable.tsx
  • mosu-admin/src/pages/DashboardPage.tsx
🧰 Additional context used
🧬 Code graph analysis (9)
mosu-admin/src/hooks/common/useSearchByName.tsx (1)
mosu-admin/src/hooks/common/index.ts (1)
  • useSearchByName (2-2)
mosu-admin/src/routes.tsx (2)
mosu-admin/src/constants/routes.ts (1)
  • ROUTES (1-26)
mosu-admin/src/pages/StudentsPage.tsx (1)
  • StudentsPage (12-201)
mosu-admin/src/hooks/common/useSortOrder.tsx (1)
mosu-admin/src/hooks/common/index.ts (1)
  • useSortOrder (4-4)
mosu-admin/src/pages/StudentsPage.tsx (3)
mosu-admin/src/api/dashboard/getStudentList.ts (1)
  • useGetStudentList (86-91)
mosu-admin/src/common/ui/PageWrapper.tsx (1)
  • PageWrapper (8-15)
mosu-admin/src/components/CommonPagination.tsx (1)
  • CommonPagination (12-61)
mosu-admin/src/layout/navItems.tsx (1)
mosu-admin/src/constants/routes.ts (1)
  • ROUTES (1-26)
mosu-admin/src/hooks/common/usePagination.tsx (1)
mosu-admin/src/hooks/common/index.ts (1)
  • usePagination (1-1)
mosu-admin/src/api/dashboard/getStudentList.ts (1)
mosu-admin/src/api/application/getApplications.ts (1)
  • Pageable (42-49)
mosu-admin/src/hooks/common/useSearchByPhone.tsx (1)
mosu-admin/src/hooks/common/index.ts (1)
  • useSearchByPhone (3-3)
mosu-admin/src/components/dashboard/StatSection.tsx (2)
mosu-admin/src/api/dashboard/getDashboardInfo.ts (1)
  • useGetDashboardInfo (21-26)
mosu-admin/src/components/dashboard/StatCard.tsx (1)
  • StatItem (4-10)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run DangerJS
🔇 Additional comments (11)
mosu-admin/src/hooks/common/useSearchByName.tsx (1)

18-26: Hook shape is clear and cohesive — good abstraction

The returned API is small and composable; this pairs well with the StudentsPage usage. No blocking issues.

mosu-admin/src/hooks/common/useSearchByPhone.tsx (2)

18-26: Solid, mirrored API with name-search hook

Parity with useSearchByName helps keep StudentsPage clean. Nice.


3-9: Confirm phone filter format with backend

The current implementation in useSearchByPhone.tsx simply passes the raw searchPhone value through to the API (via appliedSearchPhone), and in getStudentList.ts, that value is assigned directly to requestParams.phone. There’s no normalization of formatting (e.g., stripping dashes or spaces), so if a user enters “010-1234-5678” the backend will receive that exact string.

Please confirm the backend’s expected phone filter format:

• If the API only accepts digits (e.g., “01012345678”), update handlePhoneSearch to strip non-digits before applying the search:

- const handlePhoneSearch = () => {
-   setAppliedSearchPhone(searchPhone);
- };
+ const handlePhoneSearch = () => {
+   const normalized = searchPhone.replace(/\D/g, "");
+   setAppliedSearchPhone(normalized);
+ };

• If the backend supports formatted strings, no change is required—but consider whether you want to preserve the raw user input for display while sending a normalized value under the hood.

mosu-admin/src/hooks/common/usePagination.tsx (1)

46-51: Clean separation of concerns — hook returns exactly what the pagination UI needs

Return shape is minimal and sufficient (pages + prev/next availability). Looks good.

mosu-admin/src/constants/routes.ts (1)

7-7: Route constant addition is consistent and non-breaking

STUDENTS aligns with the new route and nav usage. Good to see this centralized in ROUTES.

mosu-admin/src/layout/navItems.tsx (2)

2-2: Icon import sanity check

All imported icons are used in this file. No dead imports.


10-14: Approve STUDENTS nav item — route and page wired correctly

  • mosu-admin/src/routes.tsx
    • Line 11 imports StudentsPage
    • Line 20 registers <Route path={ROUTES.STUDENTS} element={<StudentsPage />} />
  • mosu-admin/src/pages/StudentsPage.tsx exports the StudentsPage component
  • mosu-admin/src/layout/navItems.tsx includes the STUDENTS nav item under the dashboard

All wiring is in place, so this nav link will not be dead.

mosu-admin/src/components/CommonPagination.tsx (1)

17-20: Hook integration LGTM

The switch to usePagination simplifies the component and centralizes page-window logic.

mosu-admin/src/components/dashboard/StatSection.tsx (1)

26-45: Counts wiring LGTM; fallback to 0 is safe

The optional chaining with numeric fallback prevents NaN/undefined leaks into UI. Consider formatting with toLocaleString for readability, but current implementation is acceptable.

mosu-admin/src/api/dashboard/getStudentList.ts (2)

65-73: Param serialization: ensure consistent sort semantics

You pass filter.order separately from pageable.sort. If the backend expects sort=field,order rather than a separate order param, requests may not sort as intended. Current UI only sets order without a field.

Do we have a default sort field on the backend (e.g., createdAt)? If not, we should either:

  • include a sort field (e.g., sort: ["createdAt," + order]), or
  • change the API to accept order as you modeled.
    I can help align client and server if you share the backend contract.

75-79: Ambient BaseResponse Declaration – No Import Required

The BaseResponse<T> type is declared as an ambient type in mosu-admin/src/augmentations/api.d.ts, making it globally available throughout the admin codebase. Therefore, no explicit import of BaseResponse is needed in getStudentList.ts.

Likely an incorrect or invalid review comment.

Comment on lines +12 to +14
const { data: response } =
await api.get<BaseResponse<GetDashboardInfoResponseBody>>("/admin/dashboard");
return response.data;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

BaseResponse generic is referenced but not imported

BaseResponse<GetDashboardInfoResponseBody> is used in the axios call, but there is no import for BaseResponse. If it isn’t declared as a global ambient type, this will fail type-checking.

Run this to locate the declaration and confirm the intended import path:


🏁 Script executed:

#!/bin/bash
# Find where BaseResponse is defined and how it's exported
rg -nP '^(export\s+)?(type|interface)\s+BaseResponse\b' -C2
# Find existing imports of BaseResponse to copy the correct path
rg -nP 'import\s+type\s+\{\s*BaseResponse\s*\}' -n

Length of output: 114


Add missing import for BaseResponse

It appears there is no declaration or import of the BaseResponse type anywhere in the codebase, so this will break TypeScript compilation. You need to import BaseResponse from the module where it’s defined (or add a declaration if it doesn’t yet exist).

Please update mosu-admin/src/api/dashboard/getDashboardInfo.ts to include the correct import. For example:

+ import type { BaseResponse } from "../types";   // ← adjust the path to where BaseResponse is actually exported
📝 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 { data: response } =
await api.get<BaseResponse<GetDashboardInfoResponseBody>>("/admin/dashboard");
return response.data;
import type { BaseResponse } from "../types"; // ← adjust the path to where BaseResponse is actually exported
const { data: response } =
await api.get<BaseResponse<GetDashboardInfoResponseBody>>("/admin/dashboard");
return response.data;
🤖 Prompt for AI Agents
In mosu-admin/src/api/dashboard/getDashboardInfo.ts around lines 12 to 14, the
code uses the BaseResponse generic but does not import or declare it; add an
import for BaseResponse from the module that defines it (e.g. import {
BaseResponse } from '<path-to-shared-types>') at the top of the file, or if the
type doesn't exist yet, create and export a BaseResponse type in the
shared/types module and then import it here so the TypeScript compilation
succeeds.

Comment on lines +17 to +24
const statsData: StatItem[] = [
{
id: "total-applicants",
title: "없는 필드입니다",
value: 0,
unit: "명",
icon: <Users2 size={24} />,
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Remove placeholder stat (“없는 필드입니다”) before merging

Shipping a hard-coded placeholder can confuse admins and undermine trust in metrics.

-        {
-            id: "total-applicants",
-            title: "없는 필드입니다",
-            value: 0,
-            unit: "명",
-            icon: <Users2 size={24} />,
-        },
📝 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 statsData: StatItem[] = [
{
id: "total-applicants",
title: "없는 필드입니다",
value: 0,
unit: "명",
icon: <Users2 size={24} />,
},
const statsData: StatItem[] = [
];

@kimgho kimgho moved this to 진행중 in mosu-client Aug 23, 2025
@github-actions
Copy link
Copy Markdown

✅ Mosu Admin 빌드가 성공적으로 완료되었습니다!

📋 빌드 결과

  • ✅ DangerJS 검사 통과
  • ✅ React 앱 빌드 성공

🚀 다음 단계

코드 리뷰 후 머지하면 프로덕션에 배포됩니다.

@kimgho kimgho merged commit ec781e1 into main Aug 23, 2025
5 of 6 checks passed
@kimgho kimgho deleted the feature#335 branch August 23, 2025 17:22
@github-project-automation github-project-automation Bot moved this from 진행중 to 완료 in mosu-client Aug 23, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: 완료

Development

Successfully merging this pull request may close these issues.

[✨ 기능 요청] 관리자 대시보드 페이지 추가

1 participant