#29 #26 refactor: 대시보드 페이지 리펙토링 및 Dropdown 컴포넌트 추가#33
Conversation
📝 WalkthroughSummary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings. WalkthroughDashboard가 Changes
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: 선택값 반영(폼 상태 업데이트)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
추가 권장 점검 및 개선안 (짧게):
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 expectedSchedule.dateformat.
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: Keepcountin sync with the array.
For mock data, derivingcountfromschedule.lengthavoids 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 toformatCourseTermWithSlashnow 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 movingUserTypeContextto a dedicated module to avoid circular deps.
App → Dashboard → CourseCard → Appcreates 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_OPTIONSis 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.
CourseMenuTriggeris a JSX element recreated on each render. While this isn't a critical performance issue, if theDropdowncomponent 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학기', '여름학기', '겨울학기'], };
| 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} |
There was a problem hiding this comment.
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.
| <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> |
There was a problem hiding this comment.
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.
| <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.
| {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} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 라우트 매칭 공식 문서를 참고하세요.
There was a problem hiding this comment.
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) => {
| 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> |
There was a problem hiding this comment.
장식용 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' />🤖 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.
| <Link to='courses/create'> | ||
| <Button | ||
| color='ghostWhite' | ||
| size='compact' | ||
| content='mixed' | ||
| className='hover:opacity-70'> | ||
| <AddIcon className='w-3 h-3' /> | ||
| 추가 | ||
| </Button> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Button 컴포넌트 찾기
fd -type f -name '*[Bb]utton*' src --extension tsx --extension jsx | head -20Repository: 2025-snowCode/snowCode-Client
Length of output: 243
🏁 Script executed:
# 파일 구조 확인
git ls-files src | grep -i button | head -20Repository: 2025-snowCode/snowCode-Client
Length of output: 181
🏁 Script executed:
# Dashboard.tsx 파일 확인
cat -n src/pages/dashboard/Dashboard.tsx | head -80Repository: 2025-snowCode/snowCode-Client
Length of output: 2312
🏁 Script executed:
cat -n src/components/common/Button.tsxRepository: 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 -10Repository: 2025-snowCode/snowCode-Client
Length of output: 272
🏁 Script executed:
# 프로젝트 구조 및 라우트 설정 확인
find src -maxdepth 2 -name '*router*' -o -name '*route*' -o -name '*App*' | head -20Repository: 2025-snowCode/snowCode-Client
Length of output: 84
🏁 Script executed:
cat -n src/App.tsxRepository: 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 -nRepository: 2025-snowCode/snowCode-Client
Length of output: 3201
Link와 Button 중첩은 유효한 HTML 위반입니다. 구조 재설계가 필요합니다.
현재 코드는 <a><button/></a> 구조를 만드는데, 이는 W3C 표준 위반이며 접근성과 포커스 관리에 문제를 일으킵니다.
해결 방법:
- 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.
⚙️ Related ISSUE Number
Related #29 #26
📄 Work Description
formatDateMonthDay,formatCourseTermWithSlashLabeledDropdown에서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