Skip to content

#31 feat: student management 페이지 구현#36

Merged
JiiminHa merged 31 commits intodevelopfrom
feat/31-student-management-page
Feb 4, 2026
Merged

#31 feat: student management 페이지 구현#36
JiiminHa merged 31 commits intodevelopfrom
feat/31-student-management-page

Conversation

@JiiminHa
Copy link
Contributor

@JiiminHa JiiminHa commented Feb 3, 2026

⚙️ Related ISSUE Number

close #31



📄 Work Description

학생 관리 UI 개선:

  • 학생 목록을 표시하고, 선택(전체 선택 포함)이 가능하며, 각 학생의 진척도 표시기를 보여주는 새로운 StudentTable 컴포넌트를 추가했습니다. 이 테이블은 사용성과 시각적 명확성을 높이기 위해 새로운 CheckboxProgressIndicators 컴포넌트를 사용합니다.
  • 각 단원별 학생의 진행 상태를 시각적으로 나타내는 ProgressIndicators 컴포넌트를 도입했습니다.
  • 새로운 테이블, 진척도 표시기, 페이지네이션 레이아웃, 아이콘 지원 검색 입력을 사용하도록 StudentManagementPage를 업데이트했습니다.

재사용 가능한 UI 컴포넌트:

  • 관련 스타일이 포함된 커스터마이징 가능한 Checkbox 컴포넌트를 추가했습니다.
  • 라벨, 아이콘, 에러 메시지를 지원하는 재사용 가능한 Input 컴포넌트를 추가했습니다.
  • 페이지네이션 라이브러리 사용할 만큼 복잡하지 않아서 직접 구현했습니다.

학생 데이터 타입 및 목(Mock) 데이터:

  • 학생 진척도 및 코스 응답에 대한 새로운 타입을 정의하고, 개발 및 테스트를 위한 목 데이터를 제공했습니다.
    UI/UX 및 레이아웃 개선:

  • 커스터마이징 가능한 버튼 라벨을 허용하도록 AssignmentFormLayout을 업데이트하고, 더 나은 정렬과 반응성을 위해 레이아웃을 개선했습니다.

  • 더 유연한 크기 조정과 더 나은 수직 정렬을 위해 SurfaceCardLayout 컴포넌트를 임의 조정했습니다.

  • checkbox 전체 선택 가능하도록 구현했습니다. 커스텀 훅으로 빼기엔 사용할 부분이 없어 보여 해당 페이지에만 작성해 두었습니다.

의존성 및 리팩토링 업데이트:

  • 폼 처리를 개선하기 위해 react-hook-form을 의존성으로 추가했습니다.
  • auth/model 하위의 새로운 경로를 사용하도록 useUserStore의 import 구문을 리팩토링했습니다.
    이러한 변경 사항들은 종합적으로 관리자 인터페이스의 학생 관리 섹션에 대한 유지보수성, 사용성, 확장성을 향상시킵니다.


📷 Screenshot

스크린샷 2026-02-03 오후 8 49 53



💬 To Reviewers

  • 백엔드 부분 페이지네이션 구현 안되어 있어서 요청 예정이라 그냥 하드코딩 해두었습니다.
  • 아무리 봐도 디자인 도저히 이 상태로 안될 것 같아 전체적인 사이즈를 최대한 비율 유지하며 대폭 줄였습니다.
  • 개인적으로 페이지네이션 지금 디자인에서는 3쪽까지만 보여주고 나머지는 안나타나던데 그렇게 안하는 게 UX적으로도 더 나을 것 같고 애초에 그렇게 많은 페이지가 생기지 않을 것 같아 일단 구현해두었습니다. 이 부분도 의논해 보면 좋을 것 같아요.



🔗 Reference

@JiiminHa JiiminHa requested a review from suminb99 February 3, 2026 11:59
@JiiminHa JiiminHa self-assigned this Feb 3, 2026
@JiiminHa JiiminHa added 🧩 feature 기능 개발 🛠️ refactor 코드 리팩토링 🎨 ui CSS 등 사용자 UI 디자인 변경 labels Feb 3, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 3, 2026

Warning

Rate limit exceeded

@JiiminHa has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 13 minutes and 18 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

학생 관리 페이지 리팩토링 및 공통 UI 추가: Checkbox, Input, ProgressIndicators, Pagination, StudentTable 컴포넌트와 학생 도메인 타입/목 데이터 추가, AssignmentFormLayout·SurfaceCard·Layout 레이아웃 조정, react-hook-form 의존성 추가, useUserStore import 경로가 auth 모듈로 이동됨.

Changes

Cohort / File(s) Summary
의존성
package.json
react-hook-form 추가 (^7.71.1).
useUserStore import 변경
src/App.tsx, src/components/common/Header.tsx, src/pages/course-overview/ui/CourseContent.tsx, src/pages/course-overview/ui/CourseHero.tsx, src/pages/dashboard/Dashboard.tsx, src/pages/dashboard/ui/CourseCard.tsx
@/entities/user/model/useUserStore@/entities/auth/model/useUserStore로 경로 일괄 변경. 주의: 실제 auth 훅 시그니처/행동이 동일한지 확인 필요.
공통 입력/선택 컴포넌트
src/components/common/Input.tsx, src/components/common/Checkbox.tsx, src/components/common/Checkbox.module.css
ForwardRef Input(라벨/아이콘/에러), 제어형 Checkbox 및 CSS 모듈 신규 추가 — 폼 통합 시 props 및 스타일 일관성 점검 필요.
진행·페이징 UI
src/components/common/ProgressIndicators.tsx, src/shared/ui/pagination/pagination.tsx
진행 상태 원형 표시기 및 Pagination 컴포넌트 신규 추가 — 접근성(aria)·비활성 상태 로직 확인 권장.
학생 타입·목 데이터
src/entities/student/model/types.ts, src/entities/student/model/mock.ts
ProgressStatus 등 타입 추가 및 mockCourseStudents 기본 목 데이터 추가(export default). API 형태와 호환되는지 확인 필요.
학생 테이블 UI
src/entities/student/studentTable.tsx
StudentTable 신규 추가 — 다중 선택(select-all), 링크, 점수, ProgressIndicators 사용. 선택 상태 관리 로직(Set 사용)과 페이지/필터 연동 점검 필요.
관리자 폼·레이아웃 조정
src/components/admin/assignments/AssignmentFormLayout.tsx, src/components/common/SurfaceCard.tsx, src/layout/Layout.tsx
AssignmentFormLayout에 cancelLabel/confirmLabel 추가 및 레이아웃 간결화, SurfaceCard 기본 클래스(w→min-w 변경), Layout 패딩/정렬 조정. 스타일 영향 범위 확인 권장.
학생 관리 페이지 적용
src/pages/admin/student/studentManagementPage.tsx
useSearchParams 제거, react-hook-form 통합, local currentPage 상태 도입, Input/StudentTable/Pagination과 mockCourseStudents로 데이터 공급, AssignmentFormLayout 버튼 라벨 조정. 라우팅/쿼리 의존성 제거에 따른 동작 변화 점검 필요.
글로벌 스타일
src/index.css
--color-badge-red 토큰과 button 기본/disabled 스타일 추가. 기존 버튼 스타일 충돌 여부 확인 권장.

Sequence Diagram

sequenceDiagram
    actor User
    participant SMP as StudentManagementPage
    participant Mock as mockCourseStudents
    participant Form as AssignmentFormLayout
    participant Input as Input
    participant Table as StudentTable
    participant Checkbox as Checkbox
    participant Pagination as Pagination

    User->>SMP: 페이지 요청/진입
    SMP->>Mock: 목 데이터 로드
    Mock-->>SMP: course, students 반환
    SMP->>Form: 데이터와 라벨 전달
    SMP->>Table: students 렌더링
    Table->>Checkbox: 행별/전체 체크박스 렌더
    User->>Checkbox: 체크/언체크
    Checkbox-->>Table: onChange(checked)
    Table->>Table: 선택 상태 업데이트
    User->>Input: 검색어 입력
    Input->>SMP: react-hook-form 이벤트
    SMP->>Table: 필터링된 students 전달
    User->>Pagination: 페이지 변경
    Pagination->>SMP: onPageChange(page)
    SMP->>Table: 해당 페이지 students 전달
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • #21 feat: 배지 컴포넌트 구현 #24 — AssignmentFormLayout 관련 props·레이아웃 변경이 중복될 가능성 있음.
  • 2025-snowCode/snowCode-22 — AssignmentFormLayout 및 버튼/레이아웃 처리 방식과 겹칠 수 있음.
  • 2025-snowCode/snowCode-18 — AssignmentFormLayout의 이전 변경과 직접적인 연속성(수정/대체) 가능성 있음.

Suggested reviewers

  • suminb99
🚥 Pre-merge checks | ✅ 4 | ❌ 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 (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경 사항인 학생 관리 페이지 구현을 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명이 변경 사항과 밀접하게 관련되어 있으며, 구현된 컴포넌트, 타입, 레이아웃 개선 사항을 자세히 설명합니다.
Linked Issues check ✅ Passed PR은 #31 이슈의 주요 목표를 충족합니다: 학생 관리 페이지 디자인 수정, 학번 추가 기능, formInput 공통 컴포넌트 생성이 모두 구현되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경 사항이 학생 관리 페이지 구현과 관련된 범위 내에 있습니다. useUserStore 경로 변경도 이 기능 구현의 필요한 리팩토링입니다.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/31-student-management-page

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

🤖 Fix all issues with AI agents
In `@src/components/common/Checkbox.module.css`:
- Around line 1-25: Add a visible keyboard focus indicator for the custom
checkbox by styling the .checkbox:focus-visible state (or .checkbox:focus for
broader support) so keyboard users can see focus; implement a clear,
high-contrast outline or focus ring (e.g., outline or box-shadow) and use
outline-offset to avoid layout shift, using an existing or new CSS variable like
--color-focus for the color; ensure this only appears for keyboard focus (prefer
:focus-visible) and does not change the checkbox's size or break the
absolute-positioned ::after check mark.

In `@src/components/common/Checkbox.tsx`:
- Around line 3-16: CheckboxProps currently only declares checked, onChange and
className which blocks standard input attributes; update CheckboxProps to extend
React.InputHTMLAttributes<HTMLInputElement> (or
InputHTMLAttributes<HTMLInputElement>) and keep checked/onChange typing, then
accept ...rest in the Checkbox component props and spread {...rest} onto the
<input> element so attributes like aria-label, id, disabled, name, etc. are
forwarded; ensure className is merged with styles.checkbox and the onChange
handler still maps e.target.checked to the provided onChange.

In `@src/components/common/Input.tsx`:
- Around line 1-28: The Input component (the forwardRef functional component)
needs accessible associations: generate a stable id with React's useId inside
the component, set the label's htmlFor to that id, set the input's id to that
id, and when error exists create an errorId (e.g. `${id}-error`) and set input
aria-describedby to that errorId and aria-invalid to true; render the error
<span> with that errorId so screen readers can associate the message. Make these
changes in the forwardRef Input implementation (where label, error, icon,
className, props and ref are handled) so the label click focuses the input and
assistive tech sees the error.

In `@src/components/common/ProgressIndicators.tsx`:
- Around line 8-31: The progress indicators currently convey state only via
color in the ProgressIndicators component (see getColorClass and the
statuses.map rendering), which fails WCAG 1.4.1; update the rendering of each
dot to include accessible text by adding an aria-label and title that map
progress.status (e.g., 'PASSED', 'FAILED', 'PARTIAL', 'NOT_SUBMITTED', default)
to human-readable labels like "Passed", "Failed", "Partial submission", "Not
submitted" so screen readers and color-impaired users get the meaning; keep the
visual colors from getColorClass but attach the corresponding aria-label/title
to the <div> created in statuses.map to provide both tooltip and assistive-text.

In `@src/components/common/SurfaceCard.tsx`:
- Around line 6-10: The base Tailwind classes in SurfaceCard (the base string
containing min-w-[900px]) force horizontal scroll on small viewports; change
that base to remove the hard min-width by using min-w-0 and add a
large-screen-only minimum width (e.g., lg:min-w-[900px]) so the component stays
flexible on mobile while preserving the 900px minimum at lg and up; update the
base string in SurfaceCard and keep the existing size variants (medium/large
with lg: breakpoints) as-is.

In `@src/entities/student/studentTable.tsx`:
- Around line 41-56: The Checkbox component lacks support for aria-label so
screenreaders can't describe row/whole-table selections; update the Checkbox
implementation by adding an optional aria-label?: string to CheckboxProps in
src/components/common/Checkbox.tsx and forward that prop to the underlying input
element, then update the usages in studentTable.tsx: set the header Checkbox
(handleSelectAll) to aria-label="전체 학생 선택" and each row Checkbox
(handleSelectStudent / selectedStudents) to aria-label={`${student.name} 선택`} so
both the select-all and per-student checkboxes are accessible.
- Around line 11-35: When the students prop changes, reset or synchronize
selection to avoid header/row mismatch: inside StudentTable add a useEffect that
watches students and either clears selectedStudents and sets selectAll to false
or rebuilds selectedStudents by intersecting existing IDs with
students.map(s=>s.id) and then updates selectAll to (newSelected.size ===
students.length); ensure useEffect imports React/useEffect and call
setSelectedStudents and setSelectAll from within it so
handleSelectAll/handleSelectStudent remain consistent.

In `@src/pages/admin/student/studentManagementPage.tsx`:
- Around line 21-26: The search Input currently only uses a placeholder which is
not accessible to screen readers; update the Input component instance (the Input
that spreads {...register('studentSearch')} and uses icon={<Search />}) to pass
a semantic label via the label prop (e.g., label="학번 검색" or "학생 검색") so the
component renders an associated <label> element and improves accessibility for
the studentSearch field.
- Around line 11-48: currentPage is tracked but not applied to the shown list:
compute a pageSize (use the same value passed to Pagination, e.g., pageSize =
10), derive const allStudents = mockCourseStudents.response.students, compute
const totalItems = allStudents.length and pass that to Pagination instead of
hardcoded 100, then compute const start = (currentPage - 1) * pageSize and const
pagedStudents = allStudents.slice(start, start + pageSize) and pass
pagedStudents to <StudentTable />; keep setCurrentPage as the Pagination
onPageChange handler and ensure Pagination receives the dynamic totalItems so
buttons/disabled state reflect actual data.

In `@src/shared/ui/pagination/pagination.tsx`:
- Around line 14-18: Validate pageSize before computing totalPages in the
pagination component: ensure pageSize is a positive integer (e.g., if pageSize
<= 0 or not finite, either return null or set pageSize to a safe default like
1), then compute totalPages = Math.ceil(totalItems / pageSize) and keep the
existing early-return when totalPages === 0; update any uses of totalPages
(Array.from(...) and rendering logic) to rely on this validated value to prevent
Infinity/negative lengths or runtime errors. Reference symbols: pageSize,
totalItems, totalPages, and the pagination render that uses Array.from.
- Around line 22-44: The previous/next pagination buttons that render only the
ChevronDown icon lack accessible names and a button type; update the two
icon-only button elements (the ones with onClick={() => onPageChange(currentPage
- 1)} and onClick={() => onPageChange(currentPage + 1)}) to include
type="button" and descriptive aria-labels (e.g., aria-label="Previous page" and
aria-label="Next page") so screen readers can announce their purpose and to
prevent unintended form submissions. Ensure these attributes are applied to the
buttons that use <ChevronDown ... /> and keep existing disabled and onClick
logic intact.
🧹 Nitpick comments (3)
src/shared/ui/pagination/pagination.tsx (1)

3-13: Props 타입을 PaginationProps로 분리해 컨벤션 준수하기

현재 함수 파라미터에 인라인으로 정의된 타입이 있는데, 프로젝트의 Props 타입: 컴포넌트명Props 규칙과 맞지 않습니다. 타입을 별도 인터페이스로 추출하면 재사용성이 높아지고 컴포넌트 구조가 더 명확해집니다.

제안 수정안
+interface PaginationProps {
+  totalItems: number;
+  pageSize: number;
+  currentPage: number;
+  onPageChange: (page: number) => void;
+}
+
-export default function Pagination({
-  totalItems,
-  pageSize,
-  currentPage,
-  onPageChange,
-}: {
-  totalItems: number;
-  pageSize: number;
-  currentPage: number;
-  onPageChange: (page: number) => void;
-}) {
+export default function Pagination({
+  totalItems,
+  pageSize,
+  currentPage,
+  onPageChange,
+}: PaginationProps) {

참고: TypeScript 공식 문서 - Interfaces에서도 React 컴포넌트의 props 타입 관리에 인터페이스 사용을 권장합니다.

src/entities/student/model/mock.ts (1)

3-41: studentCount를 배열 길이로 파생하면 유지보수 실수 감소

현재 studentCount: 2가 수동 값으로 하드코딩되어 있어, 나중에 students 배열을 수정할 때 불일치가 발생할 수 있습니다. 학생 배열을 별도 변수로 추출하고 students.length로 계산하면 데이터 일관성을 자동으로 유지할 수 있습니다.

🧩 제안 수정안
+const students = [
+  {
+    id: 1,
+    studentId: "2210456",
+    name: "백수민",
+    score: 8,
+    totalScore: 20,
+    progress: [
+      { status: "PASSED" },
+      { status: "NOT_SUBMITTED" },
+      { status: "PARTIAL" },
+      { status: "FAILED" },
+      { status: "NOT_SUBMITTED" },
+    ]
+  },
+  {
+    id: 2,
+    studentId: "2313398",
+    name: "주아정",
+    score: 20,
+    totalScore: 20,
+    progress: [
+      { status: "PASSED" },
+      { status: "PASSED" },
+      { status: "PASSED" },
+      { status: "PASSED" },
+      { status: "PASSED" },
+    ]
+  },
+];
+
 const mockCourseStudents: CourseStudentsResponse = {
   success: true,
   response: {
     id: 1,
     title: "소프트웨어의 이해",
     section: "005",
     unitCount: 5,
-    studentCount: 2,
-    students: [
-      {
-        id: 1,
-        studentId: "2210456",
-        name: "백수민",
-        score: 8,
-        totalScore: 20,
-        progress: [
-          { status: "PASSED" },
-          { status: "NOT_SUBMITTED" },
-          { status: "PARTIAL" },
-          { status: "FAILED" },
-          { status: "NOT_SUBMITTED" },
-        ]
-      },
-      {
-        id: 2,
-        studentId: "2313398",
-        name: "주아정",
-        score: 20,
-        totalScore: 20,
-        progress: [
-          { status: "PASSED" },
-          { status: "PASSED" },
-          { status: "PASSED" },
-          { status: "PASSED" },
-          { status: "PASSED" },
-        ]
-      },
-    ]
+    studentCount: students.length,
+    students,
   }
 };

참고: Array.length 공식 문서

src/components/admin/assignments/AssignmentFormLayout.tsx (1)

24-44: 세션 내 의미 있는 구조 개선을 위해 semantic 태그 사용을 권장합니다

현재 제목, 본문, 버튼 영역이 모두 div로 처리되어 있어 HTML 문서 구조가 평면적입니다. header, section, footer 같은 semantic 태그를 사용하면:

  • 스크린리더가 페이지 구조를 정확히 인식해 접근성 향상
  • 코드 의도가 명확해져 가독성 개선
  • SEO 및 semantic web 표준 준수
제안 수정안
-        {/* 제목 */}
-        <div className='flex items-center gap-3'>
+        {/* 제목 */}
+        <header className='flex items-center gap-3'>
           <h2 className='text-black text-[24px] font-semibold'>{title}</h2>
           {titleExtra}
-        </div>
+        </header>

-        {/* 본문 */}
-        <div className='flex-1 mt-7'>{content}</div>
+        {/* 본문 */}
+        <section className='flex-1 mt-7'>{content}</section>

-        {/* 하단 버튼 */}
-        <div className='mt-7 flex justify-end gap-[21px]'>
+        {/* 하단 버튼 */}
+        <footer className='mt-7 flex justify-end gap-[21px]'>
           <Button color='outlinePurple' onClick={onCancel}>
             {cancelLabel}
           </Button>
           <Button color='primary' onClick={onConfirm}>
             {confirmLabel}
           </Button>
-        </div>
+        </footer>

참고: MDN - Content Sectioning

Comment on lines +1 to +25
.checkbox {
appearance: none;
width: 18px;
height: 18px;
background-color: var(--color-white);
border: 1px solid var(--color-primary);
border-radius: 5px;
cursor: pointer;
position: relative;
transition: all 0.2s;
vertical-align: middle;
}

.checkbox:checked::after {
content: '';
display: block;
width: 10px;
height: 10px;
background-color: var(--color-primary);
border-radius: 2px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
Copy link

@coderabbitai coderabbitai bot Feb 3, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# 파일 존재 확인 및 내용 검증
find . -name "Checkbox.module.css" -o -name "Checkbox.module.css" | head -20

Repository: 2025-snowCode/snowCode-Client

Length of output: 116


🏁 Script executed:

# 파일 내용 확인
cat -n src/components/common/Checkbox.module.css 2>/dev/null || echo "File not found in expected location"

Repository: 2025-snowCode/snowCode-Client

Length of output: 748


🏁 Script executed:

# 체크박스 관련 CSS 파일 검색
find . -type f -name "*checkbox*" -o -name "*Checkbox*" | grep -E "\.(css|tsx?|jsx?)$"

Repository: 2025-snowCode/snowCode-Client

Length of output: 153


🏁 Script executed:

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

Repository: 2025-snowCode/snowCode-Client

Length of output: 694


🏁 Script executed:

# 프로젝트의 CSS 모듈 사용 패턴 확인
rg "focus-visible|:focus" --type css -A 2 -B 2 | head -50

Repository: 2025-snowCode/snowCode-Client

Length of output: 55


🏁 Script executed:

# 접근성 관련 가이드라인 확인
find . -name "README*" -o -name "STYLE*" -o -name "*.md" | xargs grep -l "accessibility\|a11y\|focus" 2>/dev/null | head -5

Repository: 2025-snowCode/snowCode-Client

Length of output: 55


🏁 Script executed:

# 다른 폼 요소들의 포커스 처리 확인
find src/components -name "*.module.css" -type f | xargs grep -l "appearance\|:focus"

Repository: 2025-snowCode/snowCode-Client

Length of output: 114


🏁 Script executed:

# Input 관련 컴포넌트 확인
find src/components -name "*Input*" -o -name "*input*" | head -10

Repository: 2025-snowCode/snowCode-Client

Length of output: 147


🏁 Script executed:

# 프로젝트의 CSS 변수 확인
rg "color-primary|focus" --type css | head -20

Repository: 2025-snowCode/snowCode-Client

Length of output: 330


🏁 Script executed:

# Input 컴포넌트의 포커스 처리 확인
cat -n src/components/common/Input.tsx | head -50

Repository: 2025-snowCode/snowCode-Client

Length of output: 1440


🏁 Script executed:

# LabeledInput 컴포넌트 확인
cat -n src/components/admin/form/LabeledInput.tsx | head -50

Repository: 2025-snowCode/snowCode-Client

Length of output: 1006


🏁 Script executed:

# 프로젝트의 전체 CSS 스타일 규칙 확인
rg "outline|border.*focus|:focus" --type css -B 2 -A 2 | head -60

Repository: 2025-snowCode/snowCode-Client

Length of output: 55


키보드 접근성: 포커스 표시 추가 필요

appearance: none으로 기본 포커스 스타일을 제거했지만 대체 표시가 없습니다. 탭 키로 체크박스를 탐색하는 키보드 사용자가 현재 포커스된 요소를 볼 수 없어 접근성 기준(WCAG 2.1 2.4.7)을 위반합니다.

🔧 제안 패치
 .checkbox:checked::after {
   content: '';
   display: block;
   width: 10px;
   height: 10px;
   background-color: var(--color-primary);
   border-radius: 2px;
   position: absolute;
   top: 50%;
   left: 50%;
   transform: translate(-50%, -50%);
 }
+
+.checkbox:focus-visible {
+  outline: 2px solid var(--color-primary);
+  outline-offset: 2px;
+}

참고: MDN - :focus-visible

🤖 Prompt for AI Agents
In `@src/components/common/Checkbox.module.css` around lines 1 - 25, Add a visible
keyboard focus indicator for the custom checkbox by styling the
.checkbox:focus-visible state (or .checkbox:focus for broader support) so
keyboard users can see focus; implement a clear, high-contrast outline or focus
ring (e.g., outline or box-shadow) and use outline-offset to avoid layout shift,
using an existing or new CSS variable like --color-focus for the color; ensure
this only appears for keyboard focus (prefer :focus-visible) and does not change
the checkbox's size or break the absolute-positioned ::after check mark.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

체크박스 디자인이랑 색상도 겹쳐서 포커스 색상 설정하기 애매합니다

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

@JiiminHa 이해합니다! 체크박스 테두리가 이미 primary 색상을 사용하고 있어서 같은 색상으로 포커스를 표시하면 구분이 어렵겠네요. 더 나은 대안을 제안하기 위해 프로젝트의 색상 변수를 확인해보겠습니다.


🏁 Script executed:

#!/bin/bash
# 프로젝트의 CSS 색상 변수 확인
cat src/index.css | grep -E "color-|--color" -A 1 || rg "color-" --type css -A 1 | head -40

Length of output: 527


프로젝트의 색상 팔레트를 확인했습니다. 체크박스 디자인과 명확히 구분되는 포커스 스타일 대안을 제안드립니다:

추천 방법 1: box-shadow로 부드러운 링 효과 (가장 모던하고 권장)

.checkbox:focus-visible {
  box-shadow: 0 0 0 3px rgba(133, 108, 255, 0.3);
}
  • primary 색상의 반투명 그림자로 명확하면서도 디자인과 조화로움

추천 방법 2: 어두운 색상의 outline

.checkbox:focus-visible {
  outline: 2px solid var(--color-secondary-black);
  outline-offset: 2px;
}
  • secondary-black(#555267) 사용으로 primary 테두리와 명확히 구분

추천 방법 3: outline-offset 활용

.checkbox:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 3px;
}
  • 기존 테두리와 3px 간격을 두어 명확히 구분

개인적으로는 방법 1을 추천드립니다. 브랜드 색상을 유지하면서도 포커스 상태가 명확하게 표시되고, 현대적인 UI 트렌드에도 부합합니다.


🧠 Learnings used
Learnt from: JiiminHa
Repo: 2025-snowCode/snowCode-Client PR: 6
File: src/App.tsx:6-11
Timestamp: 2025-07-16T12:33:43.867Z
Learning: JiiminHa prefers hierarchical routing structure with role-based path organization: common routes at root level (/), student-specific routes under /student prefix, and admin-specific routes under /admin prefix, using nested routes with a shared Layout component for consistent UI patterns.

Learnt from: JiiminHa
Repo: 2025-snowCode/snowCode-Client PR: 2
File: src/components/Header/StudentHeader.tsx:6-18
Timestamp: 2025-07-13T00:25:00.538Z
Learning: User JiiminHa prefers using semantic HTML tags over excessive div usage for better code structure and accessibility in React components.

Comment on lines 11 to 48
const [currentPage, setCurrentPage] = useState(1);
const {register} = useForm();

const { response } = mockCourseStudents;

const titleExtra = (
<div className='flex justify-between items-center flex-1'>
<button className=' text-primary px-3 py-[6px] rounded-4xl inline-flex h-8 justify-center items-center border border-primary'>
{course || '전체'}
{response.title} ({response.section})
</button>
<div className='relative'>
<input
type='text'
placeholder='학번을 검색하세요'
className='border border-gray-300 rounded-lg px-4 py-2 pr-10 w-64'
/>
<Search className='absolute right-3 top-3 w-4 h-4 text-gray-400' />
</div>
<Input
type='text'
placeholder='학번을 검색하세요'
icon={<Search className='w-4 h-4' />}
{...register('studentSearch')}
/>
</div>
);

return (
<AssignmentFormLayout
title='학생 관리'
titleExtra={titleExtra}
content={<div>학생 관리 기능이 여기에 들어갑니다.</div>}
onCancel={() => {}}
onConfirm={() => {}}
/>
<div className='flex flex-col items-center gap-6'>
<AssignmentFormLayout
title='학생 관리'
titleExtra={titleExtra}
content={<StudentTable students={response.students} />}
onCancel={() => {}}
onConfirm={() => {}}
cancelLabel='삭제'
confirmLabel='등록'
/>
<Pagination
totalItems={100}
pageSize={10}
currentPage={currentPage}
onPageChange={(page) => {
setCurrentPage(page);
}}
/>
Copy link

@coderabbitai coderabbitai bot Feb 3, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "studentManagementPage.tsx" -type f

Repository: 2025-snowCode/snowCode-Client

Length of output: 124


🏁 Script executed:

# 파일 내용 확인
if [ -f "src/pages/admin/student/studentManagementPage.tsx" ]; then
  wc -l "src/pages/admin/student/studentManagementPage.tsx"
fi

Repository: 2025-snowCode/snowCode-Client

Length of output: 125


🏁 Script executed:

# 더 넓은 범위에서 검색
find . -path "*/admin/student/*.tsx" -type f 2>/dev/null | head -20

Repository: 2025-snowCode/snowCode-Client

Length of output: 124


🏁 Script executed:

cat -n src/pages/admin/student/studentManagementPage.tsx

Repository: 2025-snowCode/snowCode-Client

Length of output: 2043


🏁 Script executed:

# 모의 데이터 파일 위치 확인
find . -path "*/entities/student/model/mock*" -type f

Repository: 2025-snowCode/snowCode-Client

Length of output: 109


🏁 Script executed:

# 모의 데이터 파일 확인
cat -n src/entities/student/model/mock.ts 2>/dev/null || cat -n src/entities/student/model/mock.js

Repository: 2025-snowCode/snowCode-Client

Length of output: 1324


🏁 Script executed:

# StudentTable 컴포넌트 확인
find . -path "*/entities/student/studentTable*" -type f

Repository: 2025-snowCode/snowCode-Client

Length of output: 112


🏁 Script executed:

cat -n src/entities/student/studentTable.tsx

Repository: 2025-snowCode/snowCode-Client

Length of output: 3300


페이지네이션이 실제 데이터에 반영되지 않아 UX 혼란이 발생합니다.

currentPage 상태는 생성되었지만 학생 목록을 필터링하는 데 사용되지 않고 있습니다. 페이지를 전환해도 표시되는 목록이 변하지 않으므로, 최소한 프론트에서 데이터를 슬라이싱하거나 버튼을 비활성화하는 처리가 필요합니다.

추가로 totalItems가 하드코딩된 100으로 설정되어 있지만, 모의 데이터는 학생 2명만 포함하고 있어 불일치가 발생합니다.

🔧 제안 패치(프론트 슬라이싱)
 export default function StudentManagementPage() {
   const [currentPage, setCurrentPage] = useState(1);
+  const pageSize = 10;
   const {register} = useForm();
 
   const { response } = mockCourseStudents;
+  const pagedStudents = response.students.slice(
+    (currentPage - 1) * pageSize,
+    currentPage * pageSize
+  );
...
       <AssignmentFormLayout
         title='학생 관리'
         titleExtra={titleExtra}
-        content={<StudentTable students={response.students} />}
+        content={<StudentTable students={pagedStudents} />}
         onCancel={() => {}}
         onConfirm={() => {}}
         cancelLabel='삭제'
         confirmLabel='등록'
       />
       <Pagination
-        totalItems={100}
-        pageSize={10}
+        totalItems={response.students.length}
+        pageSize={pageSize}
         currentPage={currentPage}
         onPageChange={(page) => {
           setCurrentPage(page);
         }}
       />

참고: Array.prototype.slice() - MDN Web Docs

🤖 Prompt for AI Agents
In `@src/pages/admin/student/studentManagementPage.tsx` around lines 11 - 48,
currentPage is tracked but not applied to the shown list: compute a pageSize
(use the same value passed to Pagination, e.g., pageSize = 10), derive const
allStudents = mockCourseStudents.response.students, compute const totalItems =
allStudents.length and pass that to Pagination instead of hardcoded 100, then
compute const start = (currentPage - 1) * pageSize and const pagedStudents =
allStudents.slice(start, start + pageSize) and pass pagedStudents to
<StudentTable />; keep setCurrentPage as the Pagination onPageChange handler and
ensure Pagination receives the dynamic totalItems so buttons/disabled state
reflect actual data.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

api 연동 시 수정 예정

Copy link

Choose a reason for hiding this comment

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

@JiiminHa, 네, 알겠습니다! API 연동 시 함께 처리하시면 되겠네요. 👍

백엔드 페이지네이션이 구현되면 자연스럽게 해결될 부분이니, 그때 실제 데이터 기반으로 정리하시면 좋을 것 같습니다.


🧠 Learnings used
Learnt from: JiiminHa
Repo: 2025-snowCode/snowCode-Client PR: 2
File: src/components/Header/StudentHeader.tsx:10-10
Timestamp: 2025-07-13T00:27:31.464Z
Learning: Using TypeScript interfaces for React component props (like StudentHeaderProps) provides clear structure, improves readability, and makes components more maintainable and extensible for future development.

Learnt from: JiiminHa
Repo: 2025-snowCode/snowCode-Client PR: 6
File: src/App.tsx:6-11
Timestamp: 2025-07-16T12:33:43.867Z
Learning: JiiminHa prefers hierarchical routing structure with role-based path organization: common routes at root level (/), student-specific routes under /student prefix, and admin-specific routes under /admin prefix, using nested routes with a shared Layout component for consistent UI patterns.

Learnt from: JiiminHa
Repo: 2025-snowCode/snowCode-Client PR: 2
File: src/components/Header/StudentHeader.tsx:6-18
Timestamp: 2025-07-13T00:25:00.538Z
Learning: User JiiminHa prefers using semantic HTML tags over excessive div usage for better code structure and accessibility in React components.

Copy link
Contributor

@suminb99 suminb99 left a comment

Choose a reason for hiding this comment

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

수고하셨습니다!

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

🤖 Fix all issues with AI agents
In `@src/components/common/ProgressIndicators.tsx`:
- Around line 8-25: STATUS_CONFIG may not contain every possible
progress.status, so reading STATUS_CONFIG[progress.status] can yield undefined
and cause runtime errors when accessing config.label or config.color; update the
code that derives config (using STATUS_CONFIG and progress.status) to provide a
safe default entry (e.g., a DEFAULT_STATUS with fallback color and label) and
use that fallback when STATUS_CONFIG[progress.status] is undefined so components
rendering config.label and config.color never access undefined; refer to
STATUS_CONFIG and the variable/const where you compute config from
progress.status to implement this defaulting.
🧹 Nitpick comments (5)
src/shared/ui/pagination/pagination.tsx (2)

18-18: 느슨한 동등 비교(==) 대신 엄격한 동등 비교(===) 사용 권장

JavaScript/TypeScript에서는 의도치 않은 타입 변환을 방지하기 위해 엄격한 동등 비교(===)를 사용하는 것이 모범 사례입니다.

♻️ 수정 제안
-  if (totalPages == 0) {
+  if (totalPages === 0) {

참고 문서: MDN - Equality comparisons


10-15: currentPage 유효성 검증 추가 고려

currentPage가 유효 범위(1 ~ totalPages)를 벗어나는 경우에 대한 처리가 없습니다. 외부에서 잘못된 값이 전달될 경우 예상치 못한 동작이 발생할 수 있습니다.

🛡️ 방어 로직 추가 제안
 export default function Pagination({
   totalItems,
   pageSize,
   currentPage,
   onPageChange,
 }: PaginationProps) {
+  if (pageSize <= 0) return null;
+
   const totalPages = Math.ceil(totalItems / pageSize);
 
   if (totalPages === 0) {
     return null;
   }
+
+  // currentPage가 유효 범위를 벗어나면 자동 보정
+  const safePage = Math.max(1, Math.min(currentPage, totalPages));

이렇게 하면 공유 컴포넌트로서 더 안정적으로 동작합니다.

참고 문서: React 공식 문서 - Thinking in React

src/components/common/ProgressIndicators.tsx (3)

32-32: gap-2.45는 Tailwind CSS 표준 스케일에 없는 값입니다

Tailwind v4에서 gap-2.5 같은 표준 분수 값은 지원되지만, 2.45는 임의 값이므로 bracket 문법을 사용해야 합니다.

♻️ 수정 제안
-    <div className={`flex gap-2.45 ${className}`}>
+    <div className={`flex gap-[0.6125rem] ${className}`}>

또는 가장 가까운 표준 값인 gap-2.5 사용을 고려해 주세요.

참고: Tailwind CSS Spacing


37-43: role='status'보다 role='img'가 더 적절합니다

role='status'는 동적으로 업데이트되는 라이브 영역(live region)에 사용되며, 스크린리더가 변경 사항을 자동으로 알립니다. 이 인디케이터들은 정적 표시이므로, 이미지와 유사한 의미 전달 목적의 role='img'가 더 적합합니다.

이전 리뷰에서 제안된 대로 수정하면 더 정확한 시맨틱을 제공할 수 있습니다.

♻️ 수정 제안
           <div
             key={index}
-            role='status'
+            role='img'
             aria-label={config.label}
             title={config.label}
             className={`w-6.5 h-6.5 rounded-full border border-purple-stroke ${config.color}`}
           />

참고: MDN - ARIA: status role


27-48: 코딩 가이드라인: default export 권장

가이드라인에 따르면 단일 export는 default export를 권장합니다. 팀 규칙에 따라 named export를 유지해도 되지만, 일관성을 위해 검토해 주세요.

♻️ default export 적용 예시
-export const ProgressIndicators = ({
+const ProgressIndicators = ({
   statuses,
   className = '',
 }: ProgressIndicatorsProps) => {
   // ...
 };
+
+export default ProgressIndicators;

As per coding guidelines, "단일 export는 default export 권장 (팀 규칙 우선)"

Comment on lines +8 to +25
const STATUS_CONFIG = {
PASSED: {
color: 'bg-status-green',
label: '통과',
},
NOT_SUBMITTED: {
color: 'bg-white border border-stroke',
label: '미제출',
},
PARTIAL: {
color: 'bg-status-yellow',
label: '부분 제출',
},
FAILED: {
color: 'bg-status-red',
label: '실패',
},
} as const;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

STATUS_CONFIG에 정의되지 않은 status 접근 시 런타임 에러 가능성

progress.statusSTATUS_CONFIG에 정의되지 않은 값일 경우, configundefined가 되어 config.label, config.color 접근 시 에러가 발생합니다. 타입 안전성을 위해 기본값 처리를 추가하는 것이 좋습니다.

🛡️ 기본값 처리 제안
 const STATUS_CONFIG = {
   PASSED: {
     color: 'bg-status-green',
     label: '통과',
   },
   NOT_SUBMITTED: {
     color: 'bg-white border border-stroke',
     label: '미제출',
   },
   PARTIAL: {
     color: 'bg-status-yellow',
     label: '부분 제출',
   },
   FAILED: {
     color: 'bg-status-red',
     label: '실패',
   },
 } as const;

+const DEFAULT_STATUS_CONFIG = {
+  color: 'bg-white border border-stroke',
+  label: '미정',
+} as const;

그리고 컴포넌트에서:

-        const config = STATUS_CONFIG[progress.status];
+        const config = STATUS_CONFIG[progress.status] ?? DEFAULT_STATUS_CONFIG;
🤖 Prompt for AI Agents
In `@src/components/common/ProgressIndicators.tsx` around lines 8 - 25,
STATUS_CONFIG may not contain every possible progress.status, so reading
STATUS_CONFIG[progress.status] can yield undefined and cause runtime errors when
accessing config.label or config.color; update the code that derives config
(using STATUS_CONFIG and progress.status) to provide a safe default entry (e.g.,
a DEFAULT_STATUS with fallback color and label) and use that fallback when
STATUS_CONFIG[progress.status] is undefined so components rendering config.label
and config.color never access undefined; refer to STATUS_CONFIG and the
variable/const where you compute config from progress.status to implement this
defaulting.

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

🤖 Fix all issues with AI agents
In `@src/App.tsx`:
- Line 11: The import in App.tsx references StudentManagementPage with wrong
casing; rename the component file from studentManagementPage.tsx to the
kebab-case convention student-management-page.tsx (and update its default export
component to PascalCase StudentManagementPage if needed), then update the import
in App.tsx to import StudentManagementPage from
'./pages/admin/student/student-management-page'; also search for other imports
referencing the old filename and update them to the new kebab-case path to
satisfy forceConsistentCasingInFileNames and project conventions.

In `@src/pages/admin/student/studentManagementPage.tsx`:
- Around line 7-8: The import in studentManagementPage.tsx uses the wrong casing
for StudentTable; update the import to match the actual filename
(studentTable.tsx) so the module path uses the lowercase "studentTable" instead
of "StudentTable" (i.e., fix the import that references StudentTable to
reference studentTable), and optionally enable forceConsistentCasingInFileNames
in tsconfig.json to catch similar issues early.

@JiiminHa JiiminHa merged commit 954ba9b into develop Feb 4, 2026
@JiiminHa JiiminHa deleted the feat/31-student-management-page branch February 14, 2026 00:29
@JiiminHa JiiminHa mentioned this pull request Feb 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🧩 feature 기능 개발 🛠️ refactor 코드 리팩토링 🎨 ui CSS 등 사용자 UI 디자인 변경

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 학생 관리 페이지 리팩토링 및 디자인 수정 사항 반영

2 participants