Skip to content

#29 #26 refactor: 대시보드 페이지 리펙토링 및 Dropdown 컴포넌트 추가#33

Merged
suminb99 merged 26 commits intodevelopfrom
refactor/29-dashboard-update
Jan 26, 2026
Merged

#29 #26 refactor: 대시보드 페이지 리펙토링 및 Dropdown 컴포넌트 추가#33
suminb99 merged 26 commits intodevelopfrom
refactor/29-dashboard-update

Conversation

@suminb99
Copy link
Contributor

@suminb99 suminb99 commented Jan 25, 2026

⚙️ Related ISSUE Number

Related #29 #26



📄 Work Description

  • 대시보드 페이지 리팩토링 (시맨틱 구조 정리 및 스타일 개선)
  • 페이지 이동(네비게이션) 로직 구현
  • 포맷터 유틸 추가: formatDateMonthDay, formatCourseTermWithSlash
  • LabeledDropdown에서 Dropdown 공통 컴포넌트 분리
  • LabeledDropdown, CourseManagementMenu에 공통 Dropdown 적용 및 마이그레이션

TODO

  • 삭제 확인 모달 추가
  • 수정하기 버튼 클릭 시 단원 개설 페이지로 이동하도록 변경


📷 Screenshot

Screen.Recording.2026-01-25.at.14.53.34.mov
Screen.Recording.2026-01-25.at.14.43.59.mov
Screen.Recording.2026-01-25.at.14.44.40.mov



💬 To Reviewers



🔗 Reference

@suminb99 suminb99 linked an issue Jan 25, 2026 that may be closed by this pull request
3 tasks
@suminb99 suminb99 self-assigned this Jan 25, 2026
@suminb99 suminb99 requested a review from JiiminHa January 25, 2026 05:48
@coderabbitai
Copy link

coderabbitai bot commented Jan 25, 2026

📝 Walkthrough

Summary by CodeRabbit

  • 새로운 기능

    • 관리용 드롭다운에 수정·삭제 옵션 추가
    • 새 대시보드 레이아웃(강의 목록 · 내 스케쥴) 도입 및 관련 카드/리스트 UI 재구성
    • 대시보드용 예시 데이터(강의·스케줄) 추가
  • 리팩토링

    • URL 경로 일관성 개선: 'course/…' → 'courses/…'
    • 드롭다운 사용 방식이 옵션 기반으로 변경되어 공개/연도/학기 선택 UI가 업데이트됨
  • 스타일

    • 일정 배지 스타일 조정
  • 문서/유틸

    • 날짜 및 학기 표기 포맷 유틸리티 추가

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

Dashboard가 src/pages/common에서 src/pages/dashboard로 이동하고 관련 컴포넌트/타입이 재구성되었습니다. LabeledDropdown API가 variantoptions: string[]로 변경되고 재사용 가능한 Dropdown 컴포넌트가 추가되었습니다. 라우트 경로와 대시보드용 목업 모델도 추가되었습니다.

Changes

Cohort / File(s) Summary
App routing & Dashboard entry
src/App.tsx, src/pages/common/Dashboard.tsx, src/pages/dashboard/Dashboard.tsx
기존 common Dashboard 삭제, 새 pages/dashboard/Dashboard.tsx 추가, App의 Dashboard import/경로 및 CourseOverview 라우트 경로(course/:idcourses/:id) 변경.
Old dashboard 제거
src/components/common/Dashboard/*
src/components/common/Dashboard/CourseCard.tsx, .../CourseList.tsx, .../ScheduleCard.tsx, .../ScheduleList.tsx, .../types.ts
기존 Dashboard UI 컴포넌트와 타입 파일 전부 삭제.
New dashboard UI
src/pages/dashboard/ui/*
src/pages/dashboard/ui/CourseCard.tsx, .../CourseList.tsx, .../ScheduleCard.tsx, .../ScheduleList.tsx, src/pages/dashboard/models/types.ts
Dashboard 컴포넌트들을 pages/dashboard/ui로 재구성 및 타입 재정의.
Dashboard mock models
src/pages/dashboard/models/ResponseCourseList.ts, src/pages/dashboard/models/ResponseScheduleList.ts
대시보드 바인딩용 정적 응답 데이터(responseCourseList, responseScheduleList) 추가.
LabeledDropdown → Dropdown 전환
src/components/admin/form/LabeledDropdown.tsx, src/components/common/Dropdown.tsx
LabeledDropdown의 public API를 variant에서 options: string[]로 변경하고, 내부 드롭다운 로직을 제거하여 재사용 가능한 Dropdown 컴포넌트로 위임. 새로운 Dropdown 컴포넌트 추가.
Admin 페이지 업데이트
src/pages/admin/assignments/AssignmentCreatePage.tsx, src/pages/admin/courses/CourseCreatePage.tsx
LabeledDropdown 사용부를 options 기반으로 교체(PUBLIC_OPTIONS, YEAR_OPTIONS, SEMESTER_OPTIONS 등).
모델/유틸 확장
src/models/course.ts, src/utils/course.ts
Course 모델에 Schedule, DashboardCourse, Dashboard 응답 타입 추가. 날짜/학기 포맷 유틸(formatDateMonthDay, formateCourseTermWithSlash) 추가.
Course 관리 드롭다운
src/pages/dashboard/ui/CourseManagementDropdown.tsx
강의별 관리 드롭다운(수정/삭제) 컴포넌트 추가.
Badge 스타일 소소 변경
src/components/common/Badge.tsx
schedule용 'upcoming' variant에서 border-0 유틸 제거(스타일 조정).

Sequence Diagram(s)

sequenceDiagram
    participant User as "사용자"
    participant Page as "페이지\n(예: AssignmentCreatePage)"
    participant Labeled as "LabeledDropdown\n(TriggerButton)"
    participant Dropdown as "Dropdown\n(메뉴 렌더링)"
    Note over Labeled,Dropdown: rgba(96,165,250,0.5)
    User->>Page: 폼 열기/포커스
    User->>Labeled: 클릭(트리거)
    Labeled->>Dropdown: open toggle
    Dropdown-->>User: 메뉴 표시
    User->>Dropdown: 옵션 선택
    Dropdown->>Page: onSelect(option)
    Page-->>User: 선택값 반영(폼 상태 업데이트)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

🛠️ refactor, 🎨 ui

Suggested reviewers

  • JiiminHa

추가 권장 점검 및 개선안 (짧게):

  • 원인: LabeledDropdown API 변경으로 기존 호출부 누락 가능성 있음. 개선: 코드베이스 전체에서 variant= 사용을 grep/IDE로 검색해 options로 전환 누락 여부 확인하세요.
  • 이벤트/포털 경계: Dropdown의 클릭-아웃 처리(refs/포탈 사용 시) 경계가 있을 수 있습니다. 참고 문서: https://reactjs.org/docs/refs-and-the-dom.html — 포털을 쓰면 클릭아웃 로직을 문서화해 주세요.
    칭찬: 깔끔한 재구성—모듈 이동이 산뜻하네요!
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 주요 변경사항(대시보드 리팩토링 및 Dropdown 컴포넌트)을 명확하게 반영하고 있으며, 관련 이슈 번호를 포함하고 있습니다.
Description check ✅ Passed PR 설명은 대시보드 리팩토링, 컴포넌트 분리, 유틸 추가 등 실제 변경사항과 관련된 내용을 포함하고 있으며, 스크린샷도 제공되어 있습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/29-dashboard-update

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

🤖 Fix all issues with AI agents
In `@src/components/common/Dropdown.tsx`:
- Around line 33-45: The dropdown uses non-focusable divs and li click handlers
(dropdownRef, dropDownButton, isOpen, setIsOpen, options, handleSelect,
menuClassName) so make it keyboard-accessible by: replace the clickable <div>
trigger with a <button> that has aria-haspopup="menu" and aria-expanded bound to
isOpen and handle Enter/Space to toggle via the existing setIsOpen; render the
menu container with role="menu" and each option as a focusable element (e.g.,
<button> inside <li> or li role="menuitem" with tabIndex={-1}) and move
handleSelect to the option button's onClick; add onKeyDown handlers to the menu
to support Escape (close via setIsOpen(false)) and ArrowUp/ArrowDown to move
focus between options using dropdownRef or a ref array; ensure menuClassName and
existing styling remain unchanged.

In `@src/pages/dashboard/ui/CourseCard.tsx`:
- Around line 25-31: Replace the non-focusable clickable <div> with a semantic,
focusable element (e.g., a <button type="button"> or an <a>) so keyboard users
can activate navigation; keep the same onClick handler that calls
navigate(`courses/${id}`) and preserve the layout and classes used in CourseCard
(the container that currently wraps formateCourseTermWithSlash(year, semester,
section), title, etc.), and add an accessible name (e.g., aria-label using title
or the formatted term) so the button has a clear label for screen readers.

In `@src/pages/dashboard/ui/ScheduleList.tsx`:
- Around line 13-25: The list keys currently use indices which can cause UI
mismatches; update the outer li in scheduleList.map to use a stable identifier
such as schedule.date or schedule.id (use the schedule property that uniquely
identifies that day) and update ScheduleCard's key to a stable assignment
identifier (e.g., assignment.id or a composite key like
`${schedule.date}-${assignment.assignment}`) instead of index-based keys so
React can correctly track items; change the JSX keys where scheduleList.map and
schedule.assignments.map are used (refer to scheduleList.map, the li element,
and the ScheduleCard component) to use those stable values.
🧹 Nitpick comments (9)
src/models/course.ts (1)

19-27: Clarify expected Schedule.date format.
Consider documenting the exact format (e.g., YYYY-MM-DD) to prevent ambiguity for consumers and formatting utilities.

src/pages/dashboard/models/ResponseScheduleList.ts (1)

3-58: Keep count in sync with the array.
For mock data, deriving count from schedule.length avoids accidental mismatches if the list changes.

♻️ Suggested refactor
-import type {DashboardScheduleListResponse} from '@/models/course';
-
-export const responseScheduleList: DashboardScheduleListResponse = {
-  success: true,
-  response: {
-    count: 4,
-    schedule: [
+import type {DashboardScheduleListResponse} from '@/models/course';
+
+const schedule = [
       {
         date: '2025-07-20',
         remainingDays: 0,
         assignments: [
           {
             course: '데이터구조와 알고리즘',
             section: '002',
             assignment: 'final',
           },
         ],
       },
       {
         date: '2025-07-21',
         remainingDays: 1,
         assignments: [
           {
             course: '데이터구조와 알고리즘',
             section: '002',
             assignment: 'file',
           },
         ],
       },
       {
         date: '2025-07-22',
         remainingDays: 2,
         assignments: [
           {
             course: 'test',
             section: '003',
             assignment: '확인용',
           },
         ],
       },
       {
         date: '2025-07-26',
         remainingDays: 6,
         assignments: [
           {
             course: 'Python을 활용한 데이터 분석',
             section: '005',
             assignment: '날씨 데이터를 활용한 기온 변화 분석',
           },
           {
             course: '소프트웨어의이해',
             section: '005',
             assignment: '날씨 확인',
           },
         ],
       },
-    ],
-  },
-};
+];
+
+export const responseScheduleList: DashboardScheduleListResponse = {
+  success: true,
+  response: {
+    count: schedule.length,
+    schedule,
+  },
+};
src/pages/dashboard/models/ResponseCourseList.ts (1)

3-30: Vary mock course data to improve UI coverage.
Having distinct entries helps validate layout differences and avoids looking like duplicates during testing.

♻️ Example adjustment
       {
         id: 2,
         year: 2025,
-        semester: 'FIRST',
-        section: '005',
-        title: '소프트웨어의 이해',
+        semester: 'SECOND',
+        section: '006',
+        title: '데이터구조와 알고리즘',
         description:
-          'Python 언어를 기반으로 하여 프로그래밍에 대한 기본 원리를 학습한다.',
+          '자료구조와 알고리즘의 기본 개념을 학습한다.',
         unitCount: 3,
         assignmentCount: 2,
       },
src/utils/course.ts (1)

26-33: Typo in exported utility name (formate...) — consider renaming now.
Since this is new, renaming to formatCourseTermWithSlash now will prevent the typo from spreading to more imports.

♻️ Rename suggestion (remember to update imports)
-export const formateCourseTermWithSlash = (
+export const formatCourseTermWithSlash = (
   year: number,
   semester: SemesterCode,
   section: string
 ): string => {
   return `${year}\/${formatSemester(semester)}\/${section}분반`;
 };
src/pages/dashboard/ui/CourseCard.tsx (1)

2-4: Consider moving UserTypeContext to a dedicated module to avoid circular deps.
App → Dashboard → CourseCard → App creates a cycle; extracting the context (e.g., src/contexts/UserTypeContext.tsx) will decouple modules.

src/pages/dashboard/Dashboard.tsx (1)

11-13: Consider extracting the data fetching logic for future API integration.

The component currently uses hardcoded mock data (responseCourseList). When integrating with a real API, consider using a custom hook or data-fetching library (e.g., React Query/TanStack Query) to manage loading, error states, and caching.

src/pages/dashboard/ui/CourseManagementMenu.tsx (2)

5-7: Move constants outside the component.

COURSE_MENU_OPTIONS is recreated on every render. Since it's static, define it outside the component scope.

♻️ Suggested refactor
+const COURSE_MENU_OPTIONS = ['수정하기', '삭제하기'];
+
 const CourseManagementMenu = ({courseId}: {courseId: number}) => {
   const navigate = useNavigate();
-  const COURSE_MENU_OPTIONS = ['수정하기', '삭제하기'];

19-24: Consider memoizing the trigger element.

CourseMenuTrigger is a JSX element recreated on each render. While this isn't a critical performance issue, if the Dropdown component performs reference equality checks, it could cause unnecessary re-renders.

♻️ Optional: Memoize with useMemo
+import {useMemo} from 'react';
+
 const CourseManagementMenu = ({courseId}: {courseId: number}) => {
   // ...
 
-  // 강의 관리 드롭다운 메뉴 트리거
-  const CourseMenuTrigger = (
+  const CourseMenuTrigger = useMemo(() => (
     <div className='cursor-pointer p-2'>
       <EllipsisIcon className='w-[21.2px] h-[5px]' />
     </div>
-  );
+  ), []);
src/components/admin/form/LabeledDropdown.tsx (1)

14-18: Hardcoded year options will become stale.

The year options ['2021', '2022', '2023', '2024', '2025'] are static and will need manual updates. Consider generating them dynamically.

♻️ Generate years dynamically
+const CURRENT_YEAR = new Date().getFullYear();
+const YEAR_RANGE = 5;
+
 const OPTIONS = {
   visibility: ['공개', '비공개'],
-  year: ['2021', '2022', '2023', '2024', '2025'],
+  year: Array.from({length: YEAR_RANGE}, (_, i) =>
+    String(CURRENT_YEAR - YEAR_RANGE + 1 + i)
+  ),
   semester: ['1학기', '2학기', '여름학기', '겨울학기'],
 };

Comment on lines +33 to +45
return (
<div ref={dropdownRef} className={`${className ?? ''}`}>
<div onClick={() => setIsOpen(!isOpen)}>{dropDownButton}</div>

{isOpen && (
<ul
className={`absolute z-10 overflow-auto rounded-[9px] divide-y divide-purple-stroke shadow-dropdown text-left ${menuClassName}`}>
{options.map((option) => (
<li
key={option}
className='cursor-pointer px-4 py-3 hover:bg-gray-100'
onClick={(e) => handleSelect(option, e)}>
{option}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Make the dropdown keyboard-accessible (trigger/options aren’t focusable).
Using a <div> and clickable <li> blocks keyboard navigation and screen-reader semantics. Please use buttons and add menu roles.

♿ Suggested accessible structure
-    <div ref={dropdownRef} className={`${className ?? ''}`}>
-      <div onClick={() => setIsOpen(!isOpen)}>{dropDownButton}</div>
+    <div ref={dropdownRef} className={`${className ?? ''}`}>
+      <button
+        type='button'
+        onClick={() => setIsOpen(!isOpen)}
+        aria-haspopup='menu'
+        aria-expanded={isOpen}
+        className='w-full text-left'>
+        {dropDownButton}
+      </button>
 
       {isOpen && (
         <ul
-          className={`absolute z-10 overflow-auto rounded-[9px] divide-y divide-purple-stroke shadow-dropdown text-left ${menuClassName}`}>
+          role='menu'
+          className={`absolute z-10 overflow-auto rounded-[9px] divide-y divide-purple-stroke shadow-dropdown text-left ${menuClassName}`}>
           {options.map((option) => (
-            <li
-              key={option}
-              className='cursor-pointer px-4 py-3 hover:bg-gray-100'
-              onClick={(e) => handleSelect(option, e)}>
-              {option}
-            </li>
+            <li key={option} role='none'>
+              <button
+                type='button'
+                role='menuitem'
+                className='w-full text-left px-4 py-3 hover:bg-gray-100'
+                onClick={(e) => handleSelect(option, e)}>
+                {option}
+              </button>
+            </li>
           ))}
         </ul>
       )}
     </div>
🤖 Prompt for AI Agents
In `@src/components/common/Dropdown.tsx` around lines 33 - 45, The dropdown uses
non-focusable divs and li click handlers (dropdownRef, dropDownButton, isOpen,
setIsOpen, options, handleSelect, menuClassName) so make it keyboard-accessible
by: replace the clickable <div> trigger with a <button> that has
aria-haspopup="menu" and aria-expanded bound to isOpen and handle Enter/Space to
toggle via the existing setIsOpen; render the menu container with role="menu"
and each option as a focusable element (e.g., <button> inside <li> or li
role="menuitem" with tabIndex={-1}) and move handleSelect to the option button's
onClick; add onKeyDown handlers to the menu to support Escape (close via
setIsOpen(false)) and ArrowUp/ArrowDown to move focus between options using
dropdownRef or a ref array; ensure menuClassName and existing styling remain
unchanged.

Comment on lines +25 to +31
<div
onClick={() => navigate(`courses/${id}`)}
className='cursor-pointer flex flex-col gap-1.5 pl-8 pr-6.5 py-5.5'>
<p className='text-sm font-light text-light-black'>
{formateCourseTermWithSlash(year, semester, section)}
</p>
<h3 className='text-[22px]'>{title}</h3>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use a semantic button for navigation (keyboard accessibility).
The clickable <div> isn’t focusable, so keyboard users can’t open course details.

♿ Accessible navigation element
-      <div
-        onClick={() => navigate(`courses/${id}`)}
-        className='cursor-pointer flex flex-col gap-1.5 pl-8 pr-6.5 py-5.5'>
+      <button
+        type='button'
+        onClick={() => navigate(`courses/${id}`)}
+        className='cursor-pointer flex flex-col gap-1.5 pl-8 pr-6.5 py-5.5 text-left bg-transparent border-0'>
         <p className='text-sm font-light text-light-black'>
           {formateCourseTermWithSlash(year, semester, section)}
         </p>
         <h3 className='text-[22px]'>{title}</h3>
         <p className='text-base font-light text-secondary-black line-clamp-3'>
           {description}
         </p>
-      </div>
+      </button>
📝 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
<div
onClick={() => navigate(`courses/${id}`)}
className='cursor-pointer flex flex-col gap-1.5 pl-8 pr-6.5 py-5.5'>
<p className='text-sm font-light text-light-black'>
{formateCourseTermWithSlash(year, semester, section)}
</p>
<h3 className='text-[22px]'>{title}</h3>
<button
type='button'
onClick={() => navigate(`courses/${id}`)}
className='cursor-pointer flex flex-col gap-1.5 pl-8 pr-6.5 py-5.5 text-left bg-transparent border-0'>
<p className='text-sm font-light text-light-black'>
{formateCourseTermWithSlash(year, semester, section)}
</p>
<h3 className='text-[22px]'>{title}</h3>
<p className='text-base font-light text-secondary-black line-clamp-3'>
{description}
</p>
</button>
🤖 Prompt for AI Agents
In `@src/pages/dashboard/ui/CourseCard.tsx` around lines 25 - 31, Replace the
non-focusable clickable <div> with a semantic, focusable element (e.g., a
<button type="button"> or an <a>) so keyboard users can activate navigation;
keep the same onClick handler that calls navigate(`courses/${id}`) and preserve
the layout and classes used in CourseCard (the container that currently wraps
formateCourseTermWithSlash(year, semester, section), title, etc.), and add an
accessible name (e.g., aria-label using title or the formatted term) so the
button has a clear label for screen readers.

Comment on lines +13 to +25
{scheduleList.map((schedule, index) => (
<li className='flex items-start justify-start gap-5' key={index}>
{/* 마감일 */}
<div className='w-20 shrink-0'>
<DeadLine date={schedule.date} />
</div>

{/* 스케쥴 카드 목록 */}
<ul className='flex flex-col gap-3 w-full'>
{schedule.assignments.map((assignment, idx) => (
<ScheduleCard
key={assignment.assignment + idx}
remainingDays={schedule.remainingDays}
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

Avoid index-based keys for schedules/assignments.
Use stable identifiers (e.g., date + course/section) to prevent UI mismatches when data changes.

🔧 Example key adjustments
-      {scheduleList.map((schedule, index) => (
-        <li className='flex items-start justify-start gap-5' key={index}>
+      {scheduleList.map((schedule) => (
+        <li className='flex items-start justify-start gap-5' key={schedule.date}>
...
-            {schedule.assignments.map((assignment, idx) => (
+            {schedule.assignments.map((assignment) => (
               <ScheduleCard
-                key={assignment.assignment + idx}
+                key={`${assignment.course}-${assignment.section}-${assignment.assignment}`}
                 remainingDays={schedule.remainingDays}
                 {...assignment}
               />
             ))}
🤖 Prompt for AI Agents
In `@src/pages/dashboard/ui/ScheduleList.tsx` around lines 13 - 25, The list keys
currently use indices which can cause UI mismatches; update the outer li in
scheduleList.map to use a stable identifier such as schedule.date or schedule.id
(use the schedule property that uniquely identifies that day) and update
ScheduleCard's key to a stable assignment identifier (e.g., assignment.id or a
composite key like `${schedule.date}-${assignment.assignment}`) instead of
index-based keys so React can correctly track items; change the JSX keys where
scheduleList.map and schedule.assignments.map are used (refer to
scheduleList.map, the li element, and the ScheduleCard component) to use those
stable values.

Copy link
Contributor

@JiiminHa JiiminHa 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/utils/course.ts`:
- Around line 28-35: Rename the mis-typed function formateCourseTermWithSlash to
formatCourseTermWithSlash and remove the unnecessary slash escape in its
template string (use `${year}/${formatSemester(semester)}/${section}분반`); update
all references including the import and call sites in CourseCard.tsx to the new
name, keeping uses of formatSemester and formatDate unchanged.
🧹 Nitpick comments (1)
src/App.tsx (1)

38-38: 라우트 경로 복수형 변경 - RESTful 컨벤션을 잘 따랐습니다!

courses/:id 변경은 올바르게 이루어졌고, 네비게이션 코드(CourseCard.tsx, CourseManagementDropdown.tsx, Dashboard.tsx)가 모두 일관되게 업데이트되어 있습니다. 이전 단수형 경로의 참조도 남아있지 않습니다. ✓

남은 개선 사항은 라우트 선언 순서입니다. 현재 admin 라우트에서 courses/:id (동적 세그먼트)가 courses/create (정적 세그먼트)보다 먼저 선언되어 있습니다. React Router v6+에서는 정상 동작하지만, 가독성과 유지보수성을 위해 더 구체적인 라우트를 먼저 배치하는 것이 좋습니다.

♻️ 선택적 개선안
         {/* 관리자 영역 */}
         <Route path='admin'>
           <Route index element={<Dashboard />} />
           <Route path='assignments' element={<AssignmentsPage />} />
           <Route path='assignments/create' element={<AssignmentCreatePage />} />
           <Route path='assignments/:id' element={<AssignmentSelectPage />} />
+          <Route path='courses/create' element={<CourseCreatePage />} />
           <Route path='courses/:id' element={<CourseOverviewPage />} />
-          <Route path='courses/create' element={<CourseCreatePage />} />
           <Route path='student' element={<StudentManagementPage />} />
         </Route>

📚 자세한 내용은 React Router 라우트 매칭 공식 문서를 참고하세요.

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/pages/dashboard/Dashboard.tsx`:
- Around line 39-44: The LogoIcon used in SectionHeader is decorative and should
be hidden from assistive tech; update the usage or the LogoIcon component so the
rendered SVG has aria-hidden="true" and focusable="false" (or ensure the
LogoIcon accepts and forwards these props) so screen readers ignore it; modify
the JSX in SectionHeader to pass these attributes to LogoIcon (or adjust
LogoIcon's implementation) while keeping the visual element unchanged.
- Around line 50-58: Current markup nests a Button inside a Link which produces
invalid HTML (<a><button/>) and breaks accessibility; refactor so the
interactive element is a single semantic element by either creating a wrapper
component (e.g., AddButton) that renders Link and applies Button styles, or make
the existing Button component polymorphic (add an as prop on Button and render
it as the Link component when used) and update the usage in Dashboard.tsx to use
<Button as={Link} to="courses/create" ...> or <AddButton to="courses/create">,
ensuring no nested interactive elements remain and preserving existing
props/classNames like color, size, content, and className.
♻️ Duplicate comments (1)
src/pages/dashboard/ui/CourseCard.tsx (1)

25-35: 키보드 접근성을 위해 시맨틱 요소 사용이 필요합니다.

현재 클릭 가능한 <div>는 포커스가 불가능하여 키보드 사용자가 접근할 수 없습니다. <button> 또는 <a> 태그로 변경해주세요. Based on learnings, 시맨틱 HTML 사용이 더 선호됩니다.

♿ 접근성 개선 제안
-      <div
-        onClick={() => navigate(`courses/${id}`)}
-        className='cursor-pointer flex flex-col gap-1.5 pl-8 pr-6.5 py-5.5'>
+      <button
+        type='button'
+        onClick={() => navigate(`courses/${id}`)}
+        className='cursor-pointer flex flex-col gap-1.5 pl-8 pr-6.5 py-5.5 text-left w-full bg-transparent border-0'>
         <p className='text-sm font-light text-light-black'>
           {formateCourseTermWithSlash(year, semester, section)}
         </p>
         <h3 className='text-[22px]'>{title}</h3>
         <p className='text-base font-light text-secondary-black line-clamp-3'>
           {description}
         </p>
-      </div>
+      </button>

참고: MDN - Button 접근성

🧹 Nitpick comments (1)
src/pages/dashboard/ui/CourseCard.tsx (1)

7-7: Props 타입 네이밍 컨벤션 적용을 권장합니다.

코딩 가이드라인에 따르면 Props 타입은 컴포넌트명Props 형태를 사용해야 합니다. 현재 DashboardCourse를 직접 사용하고 있어 컨벤션과 맞지 않습니다.

♻️ Props 타입 분리 제안
 import {formateCourseTermWithSlash} from '@/utils/course';
 import type {DashboardCourse} from '@/models/course';
 import CourseManagementDropdown from './CourseManagementDropdown';
 import {useNavigate} from 'react-router-dom';
 import {useUserStore} from '@/entities/user/model/useUserStore';

+type CourseCardProps = DashboardCourse;
+
-const CourseCard = (course: DashboardCourse) => {
+const CourseCard = (course: CourseCardProps) => {

Comment on lines +39 to +44
const SectionHeader = ({title}: {title: string}) => {
return (
<header className='flex items-center gap-2 mb-5'>
<LogoIcon className='w-8 h-5 shrink-0' />
<h2 className='text-xl/normal font-medium'>{title}</h2>
</header>
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

장식용 SVG는 스크린리더에서 숨겨주세요.

LogoIcon이 정보 전달 목적이 아니라면 스크린리더에 불필요한 출력이 생길 수 있습니다. aria-hidden/focusable 처리로 접근성을 개선해 주세요.

✨ 제안 수정안
-      <LogoIcon className='w-8 h-5 shrink-0' />
+      <LogoIcon className='w-8 h-5 shrink-0' aria-hidden='true' focusable='false' />
참고 문서: ``` https://developer.mozilla.org/en-US/docs/Web/SVG/Accessibility ```
🤖 Prompt for AI Agents
In `@src/pages/dashboard/Dashboard.tsx` around lines 39 - 44, The LogoIcon used in
SectionHeader is decorative and should be hidden from assistive tech; update the
usage or the LogoIcon component so the rendered SVG has aria-hidden="true" and
focusable="false" (or ensure the LogoIcon accepts and forwards these props) so
screen readers ignore it; modify the JSX in SectionHeader to pass these
attributes to LogoIcon (or adjust LogoIcon's implementation) while keeping the
visual element unchanged.

Comment on lines +50 to +58
<Link to='courses/create'>
<Button
color='ghostWhite'
size='compact'
content='mixed'
className='hover:opacity-70'>
<AddIcon className='w-3 h-3' />
추가
</Button>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Button 컴포넌트 찾기
fd -type f -name '*[Bb]utton*' src --extension tsx --extension jsx | head -20

Repository: 2025-snowCode/snowCode-Client

Length of output: 243


🏁 Script executed:

# 파일 구조 확인
git ls-files src | grep -i button | head -20

Repository: 2025-snowCode/snowCode-Client

Length of output: 181


🏁 Script executed:

# Dashboard.tsx 파일 확인
cat -n src/pages/dashboard/Dashboard.tsx | head -80

Repository: 2025-snowCode/snowCode-Client

Length of output: 2312


🏁 Script executed:

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

Repository: 2025-snowCode/snowCode-Client

Length of output: 2243


🏁 Script executed:

# 라우터 설정 파일 찾기
fd -type f \( -name '*.tsx' -o -name '*.ts' \) src | xargs rg -l "BrowserRouter|createBrowserRouter|Route.*path" | head -10

Repository: 2025-snowCode/snowCode-Client

Length of output: 272


🏁 Script executed:

# 프로젝트 구조 및 라우트 설정 확인
find src -maxdepth 2 -name '*router*' -o -name '*route*' -o -name '*App*' | head -20

Repository: 2025-snowCode/snowCode-Client

Length of output: 84


🏁 Script executed:

cat -n src/App.tsx

Repository: 2025-snowCode/snowCode-Client

Length of output: 2803


🏁 Script executed:

# 라우트 설정 파일 찾기 (main.tsx, router.ts 등)
find src -maxdepth 1 -type f -name '*.tsx' -o -name '*.ts' | xargs cat -n

Repository: 2025-snowCode/snowCode-Client

Length of output: 3201


LinkButton 중첩은 유효한 HTML 위반입니다. 구조 재설계가 필요합니다.

현재 코드는 <a><button/></a> 구조를 만드는데, 이는 W3C 표준 위반이며 접근성과 포커스 관리에 문제를 일으킵니다.

해결 방법:

  1. Button을 Link처럼 동작하게 개선:
const AddButton = () => {
  return (
    <Link to='courses/create' className='no-underline'>
      <Button
        color='ghostWhite'
        size='compact'
        content='mixed'
        className='hover:opacity-70'>
        <AddIcon className='w-3 h-3' />
        추가
      </Button>
    </Link>
  );
};

대신 Button이 Link를 내부에서 렌더링하거나, 컴포넌트가 다형성을 지원하도록 변경하는 것이 더 깔끔합니다:

const Button = ({
  as = 'button',
  ...props
}: ButtonProps & {as?: React.ElementType}) => {
  const Component = as;
  return <Component className={button(props)}>{children}</Component>;
};

// 사용
<Button as={Link} to='courses/create'>...</Button>

참고: 상대 경로 to='courses/create'는 현재 /admin 경로에서 올바르게 /admin/courses/create로 해석되므로 라우팅은 의도대로 작동합니다.

관련 문서: MDN - Interactive elements

🤖 Prompt for AI Agents
In `@src/pages/dashboard/Dashboard.tsx` around lines 50 - 58, Current markup nests
a Button inside a Link which produces invalid HTML (<a><button/>) and breaks
accessibility; refactor so the interactive element is a single semantic element
by either creating a wrapper component (e.g., AddButton) that renders Link and
applies Button styles, or make the existing Button component polymorphic (add an
as prop on Button and render it as the Link component when used) and update the
usage in Dashboard.tsx to use <Button as={Link} to="courses/create" ...> or
<AddButton to="courses/create">, ensuring no nested interactive elements remain
and preserving existing props/classNames like color, size, content, and
className.

@suminb99 suminb99 merged commit 3568435 into develop Jan 26, 2026
1 check passed
JiiminHa added a commit that referenced this pull request Feb 9, 2026
JiiminHa added a commit that referenced this pull request Feb 12, 2026
@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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor: 대시보드 페이지 리펙토링

2 participants