feat: 계획 관리 페이지 UI 구현#39
Conversation
📝 WalkthroughWalkthrough플랜 관리 기능을 위한 새로운 페이지 및 관련 UI 컴포넌트들을 추가합니다. PageHeader, AddPlanButton, PlanSection, SearchBar, TaskItem 컴포넌트를 새로 작성하고, 기존 Sidebar와 Card 컴포넌트를 일부 수정합니다. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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 |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In `@components/common/Sidebar.tsx`:
- Line 18: Update the Sidebar menu entry for "계획 관리" so its href points to the
existing plans listing route instead of a non-existent new-plan route: locate
the menu item { label: '계획 관리', href: '/plans/new', icon: CalendarPlus } in the
Sidebar component and change href to '/plans' so navigation goes to the plans
list (where AddPlanButton handles creation).
In `@components/plans/PlanSection.tsx`:
- Around line 36-55: The header toggle currently uses a clickable div wrapping a
nested button which breaks semantics and keyboard access; replace the outer div
(the element with onClick={() => setIsOpen(!isOpen)} containing title,
description, and the right-side counts/icon) with a single interactive element
(use a button) and move the click handler to that button (calling setIsOpen).
Keep the chevron icons (ChevronUp/ChevronDown) as non-interactive decorative
elements (e.g., span) and ensure the button exposes aria-expanded tied to isOpen
and preserves the existing layout and text elements (title, description,
completedCount/totalCount). Ensure no nested interactive controls remain.
- Around line 74-75: The Add-subitem button in the PlanSection component can
unintentionally submit parent forms because it lacks an explicit type; update
the JSX for the button that renders "새 하위항목 추가" inside PlanSection to include
type="button" (keeping existing className and children intact) so it won't act
as a submit control when nested in a form.
In `@components/plans/TaskItem.tsx`:
- Line 1: 파일 상단 주석이 잘못된 경로(`// components/dashboard/TaskItem.tsx`)로 되어 있어 혼동을
유발합니다; 수정하려면 파일의 최상단 주석을 실제 경로인 `// components/plans/TaskItem.tsx`로 변경하거나 불필요하면
주석을 제거하세요—수정 대상은 TaskItem 컴포넌트 파일의 파일-header 주석입니다.
- Around line 4-26: The checkbox-looking element in TaskItem is visually
interactive but has no accessibility or toggle behavior; update TaskItem to
accept an optional onToggle prop on TaskItemProps and replace or enhance the
checkbox div (the element rendering the Check icon when isCompleted is true) to
be an interactive control: use a semantic interactive element (e.g., button) or
add role="checkbox", aria-checked based on isCompleted, tabIndex, and keyboard
handling (toggle on Space/Enter) that calls onToggle, and only show
cursor-pointer when onToggle is provided; also include an accessible label
(aria-label or aria-labelledby) for the control so screen readers can announce
it.
🧹 Nitpick comments (5)
components/plans/SearchBar.tsx (1)
1-1: 파일 경로 주석이 실제 위치와 불일치주석에는
components/dashboard/SearchBar.tsx로 되어 있지만, 실제 파일 경로는components/plans/SearchBar.tsx입니다.🔧 수정 제안
-// components/dashboard/SearchBar.tsx +// components/plans/SearchBar.tsxcomponents/plans/AddPlanButton.tsx (2)
1-1: 파일 경로 주석이 실제 위치와 불일치주석에는
components/dashboard/AddPlanButton.tsx로 되어 있지만, 실제 파일 경로는components/plans/AddPlanButton.tsx입니다.🔧 수정 제안
-// components/dashboard/AddPlanButton.tsx +// components/plans/AddPlanButton.tsx
6-11: 버튼에type="button"명시 권장
<button>요소는 type 속성이 없으면 폼 내에서 기본값이"submit"이 됩니다. 의도치 않은 폼 제출을 방지하기 위해 명시적으로type="button"을 추가하는 것이 좋습니다.🔧 수정 제안
- <button className="flex w-full flex-col items-center justify-center gap-2 rounded-2xl border-2 border-dashed border-gray-300 py-6 font-medium text-gray-500 transition-all hover:border-gray-400 hover:bg-gray-50"> + <button type="button" className="flex w-full flex-col items-center justify-center gap-2 rounded-2xl border-2 border-dashed border-gray-300 py-6 font-medium text-gray-500 transition-all hover:border-gray-400 hover:bg-gray-50">app/(with-sidebar)/plans/page.tsx (2)
1-1: 사용되지 않는 import 제거
FormField가 import 되어 있지만 컴포넌트 내에서 사용되지 않습니다.🔧 수정 제안
-import FormField from '@/components/auth/FormField'; import PageHeader from '@/components/common/PageHeader';
43-84: 하드코딩된 색상값 대신 테마 변수 사용 고려통계 카드에서
#D5DCFB,#4757D3,#EBDBFC등 하드코딩된 색상값을 사용하고 있습니다. 유지보수성과 디자인 일관성을 위해 Tailwind 테마 변수로 정의하는 것을 권장합니다.현재 UI 구현 단계에서는 문제없지만, 향후 테마 시스템 구축 시 리팩토링을 고려해 주세요.
| { label: '새 계획 만들기', href: '/plans/new', icon: CalendarPlus }, | ||
| { label: '새 페이지 만들기', href: '/write', icon: CopyPlus }, | ||
| ]; | ||
| { label: '계획 관리', href: '/plans/new', icon: CalendarPlus }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find all plans-related page files
find . -path "*/plans/*" -name "*.tsx" -o -path "*/plans/*" -name "*.ts" | grep -E "(page|layout)" | head -20Repository: DeveloperBlog-Devflow/devflow
Length of output: 108
🏁 Script executed:
#!/bin/bash
# Check the Sidebar.tsx file around line 18 for context
cat -n components/common/Sidebar.tsx | sed -n '1,40p'Repository: DeveloperBlog-Devflow/devflow
Length of output: 1367
🏁 Script executed:
#!/bin/bash
# Search for all references to '/plans' routes in the codebase
rg "plans" --type tsx --type ts -i | grep -E "(href|route|path)" | head -30Repository: DeveloperBlog-Devflow/devflow
Length of output: 100
🏁 Script executed:
#!/bin/bash
# Check the plans directory structure
ls -la app/\(with-sidebar\)/plans/Repository: DeveloperBlog-Devflow/devflow
Length of output: 241
🏁 Script executed:
#!/bin/bash
# Read the plans page file
cat -n app/\(with-sidebar\)/plans/page.tsxRepository: DeveloperBlog-Devflow/devflow
Length of output: 4125
🏁 Script executed:
#!/bin/bash
# Search for all route directories under app
find app -type d -name "plans" -o -type d -name "*new*" | head -20Repository: DeveloperBlog-Devflow/devflow
Length of output: 97
href 경로 수정 필요
"/plans/new" 경로가 존재하지 않습니다. 계획 관리 페이지는 /plans 경로에만 존재하며, 여기서 AddPlanButton을 통해 새 계획 생성을 처리합니다.
href를 /plans로 변경하세요.
🤖 Prompt for AI Agents
In `@components/common/Sidebar.tsx` at line 18, Update the Sidebar menu entry for
"계획 관리" so its href points to the existing plans listing route instead of a
non-existent new-plan route: locate the menu item { label: '계획 관리', href:
'/plans/new', icon: CalendarPlus } in the Sidebar component and change href to
'/plans' so navigation goes to the plans list (where AddPlanButton handles
creation).
| <div | ||
| className="flex cursor-pointer items-start justify-between" | ||
| onClick={() => setIsOpen(!isOpen)} | ||
| > | ||
| <div> | ||
| <h3 className="text-xl font-bold text-gray-900">{title}</h3> | ||
| <p className="mt-1 text-sm text-gray-500">{description}</p> | ||
| </div> | ||
|
|
||
| <div className="flex items-center gap-4"> | ||
| <div className="text-right"> | ||
| <span className="block text-xs text-gray-500">진행률</span> | ||
| <span className="text-lg font-bold text-[#556BD6]"> | ||
| {completedCount}/{totalCount} | ||
| </span> | ||
| </div> | ||
| <button className="text-gray-400 hover:text-gray-600"> | ||
| {isOpen ? <ChevronUp size={24} /> : <ChevronDown size={24} />} | ||
| </button> | ||
| </div> |
There was a problem hiding this comment.
토글 헤더가 비접근성(div 클릭)이며 중첩 버튼 구조입니다.
키보드 조작이 불가하고, 버튼이 중첩되어 시멘틱/접근성 문제가 있습니다. 토글 영역을 단일 button으로 바꾸고 아이콘은 비인터랙티브 요소로 두는 편이 안전합니다.
🛠️ 제안 수정
- <div
- className="flex cursor-pointer items-start justify-between"
- onClick={() => setIsOpen(!isOpen)}
- >
+ <button
+ type="button"
+ className="flex w-full items-start justify-between text-left"
+ onClick={() => setIsOpen((prev) => !prev)}
+ aria-expanded={isOpen}
+ >
<div>
<h3 className="text-xl font-bold text-gray-900">{title}</h3>
<p className="mt-1 text-sm text-gray-500">{description}</p>
</div>
<div className="flex items-center gap-4">
<div className="text-right">
<span className="block text-xs text-gray-500">진행률</span>
<span className="text-lg font-bold text-[`#556BD6`]">
{completedCount}/{totalCount}
</span>
</div>
- <button className="text-gray-400 hover:text-gray-600">
+ <span className="text-gray-400 hover:text-gray-600" aria-hidden="true">
{isOpen ? <ChevronUp size={24} /> : <ChevronDown size={24} />}
- </button>
+ </span>
</div>
- </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 | |
| className="flex cursor-pointer items-start justify-between" | |
| onClick={() => setIsOpen(!isOpen)} | |
| > | |
| <div> | |
| <h3 className="text-xl font-bold text-gray-900">{title}</h3> | |
| <p className="mt-1 text-sm text-gray-500">{description}</p> | |
| </div> | |
| <div className="flex items-center gap-4"> | |
| <div className="text-right"> | |
| <span className="block text-xs text-gray-500">진행률</span> | |
| <span className="text-lg font-bold text-[#556BD6]"> | |
| {completedCount}/{totalCount} | |
| </span> | |
| </div> | |
| <button className="text-gray-400 hover:text-gray-600"> | |
| {isOpen ? <ChevronUp size={24} /> : <ChevronDown size={24} />} | |
| </button> | |
| </div> | |
| <button | |
| type="button" | |
| className="flex w-full items-start justify-between text-left" | |
| onClick={() => setIsOpen((prev) => !prev)} | |
| aria-expanded={isOpen} | |
| > | |
| <div> | |
| <h3 className="text-xl font-bold text-gray-900">{title}</h3> | |
| <p className="mt-1 text-sm text-gray-500">{description}</p> | |
| </div> | |
| <div className="flex items-center gap-4"> | |
| <div className="text-right"> | |
| <span className="block text-xs text-gray-500">진행률</span> | |
| <span className="text-lg font-bold text-[`#556BD6`]"> | |
| {completedCount}/{totalCount} | |
| </span> | |
| </div> | |
| <span className="text-gray-400 hover:text-gray-600" aria-hidden="true"> | |
| {isOpen ? <ChevronUp size={24} /> : <ChevronDown size={24} />} | |
| </span> | |
| </div> | |
| </button> |
🤖 Prompt for AI Agents
In `@components/plans/PlanSection.tsx` around lines 36 - 55, The header toggle
currently uses a clickable div wrapping a nested button which breaks semantics
and keyboard access; replace the outer div (the element with onClick={() =>
setIsOpen(!isOpen)} containing title, description, and the right-side
counts/icon) with a single interactive element (use a button) and move the click
handler to that button (calling setIsOpen). Keep the chevron icons
(ChevronUp/ChevronDown) as non-interactive decorative elements (e.g., span) and
ensure the button exposes aria-expanded tied to isOpen and preserves the
existing layout and text elements (title, description,
completedCount/totalCount). Ensure no nested interactive controls remain.
| <button className="mt-2 flex w-full items-center justify-center gap-1 rounded-xl border-2 border-dashed border-[#556BD6]/30 py-3 text-sm font-medium text-[#556BD6] transition-colors hover:bg-[#556BD6]/5"> | ||
| <Plus size={16} /> 새 하위항목 추가 |
There was a problem hiding this comment.
버튼에 type 지정이 필요합니다.
폼 내부에 포함될 경우 의도치 않은 submit이 발생할 수 있습니다.
🛠️ 제안 수정
- <button className="mt-2 flex w-full items-center justify-center gap-1 rounded-xl border-2 border-dashed border-[`#556BD6`]/30 py-3 text-sm font-medium text-[`#556BD6`] transition-colors hover:bg-[`#556BD6`]/5">
+ <button type="button" className="mt-2 flex w-full items-center justify-center gap-1 rounded-xl border-2 border-dashed border-[`#556BD6`]/30 py-3 text-sm font-medium text-[`#556BD6`] transition-colors hover:bg-[`#556BD6`]/5">📝 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.
| <button className="mt-2 flex w-full items-center justify-center gap-1 rounded-xl border-2 border-dashed border-[#556BD6]/30 py-3 text-sm font-medium text-[#556BD6] transition-colors hover:bg-[#556BD6]/5"> | |
| <Plus size={16} /> 새 하위항목 추가 | |
| <button type="button" className="mt-2 flex w-full items-center justify-center gap-1 rounded-xl border-2 border-dashed border-[`#556BD6`]/30 py-3 text-sm font-medium text-[`#556BD6`] transition-colors hover:bg-[`#556BD6`]/5"> | |
| <Plus size={16} /> 새 하위항목 추가 |
🤖 Prompt for AI Agents
In `@components/plans/PlanSection.tsx` around lines 74 - 75, The Add-subitem
button in the PlanSection component can unintentionally submit parent forms
because it lacks an explicit type; update the JSX for the button that renders "새
하위항목 추가" inside PlanSection to include type="button" (keeping existing className
and children intact) so it won't act as a submit control when nested in a form.
| @@ -0,0 +1,38 @@ | |||
| // components/dashboard/TaskItem.tsx | |||
There was a problem hiding this comment.
파일 경로 주석이 실제 위치와 불일치합니다.
현재 주석이 dashboard로 되어 있어 파일 탐색/리뷰 시 혼선을 줄 수 있습니다.
🛠️ 제안 수정
-// components/dashboard/TaskItem.tsx
+// components/plans/TaskItem.tsx📝 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.
| // components/dashboard/TaskItem.tsx | |
| // components/plans/TaskItem.tsx |
🤖 Prompt for AI Agents
In `@components/plans/TaskItem.tsx` at line 1, 파일 상단 주석이 잘못된 경로(`//
components/dashboard/TaskItem.tsx`)로 되어 있어 혼동을 유발합니다; 수정하려면 파일의 최상단 주석을 실제 경로인
`// components/plans/TaskItem.tsx`로 변경하거나 불필요하면 주석을 제거하세요—수정 대상은 TaskItem 컴포넌트
파일의 파일-header 주석입니다.
| interface TaskItemProps { | ||
| text: string; | ||
| date: string; | ||
| isCompleted: boolean; | ||
| } | ||
|
|
||
| export default function TaskItem({ text, date, isCompleted }: TaskItemProps) { | ||
| return ( | ||
| <div | ||
| className={`mb-2 flex items-center gap-3 rounded-xl p-4 transition-colors ${ | ||
| isCompleted ? 'bg-gray-50' : 'bg-gray-100' | ||
| }`} | ||
| > | ||
| {/* 체크박스 커스텀 */} | ||
| <div | ||
| className={`flex h-6 w-6 cursor-pointer items-center justify-center rounded-md border transition-colors ${ | ||
| isCompleted | ||
| ? 'border-green-500 bg-green-500 text-white' | ||
| : 'border-gray-300 bg-white hover:border-purple-400' | ||
| }`} | ||
| > | ||
| {isCompleted && <Check size={16} strokeWidth={3} />} | ||
| </div> |
There was a problem hiding this comment.
체크박스가 클릭 가능해 보이지만 동작·접근성이 없습니다.
UI만 구현하는 단계라면 클릭 affordance를 제거하거나, 실제 토글 핸들러/ARIA를 붙여 접근성을 확보해 주세요.
🛠️ 한 가지 접근: 토글 핸들러 옵션 추가 + 버튼化
interface TaskItemProps {
text: string;
date: string;
isCompleted: boolean;
+ onToggle?: () => void;
}
-export default function TaskItem({ text, date, isCompleted }: TaskItemProps) {
+export default function TaskItem({ text, date, isCompleted, onToggle }: TaskItemProps) {
+ const isInteractive = typeof onToggle === 'function';
return (
<div
className={`mb-2 flex items-center gap-3 rounded-xl p-4 transition-colors ${
isCompleted ? 'bg-gray-50' : 'bg-gray-100'
}`}
>
{/* 체크박스 커스텀 */}
- <div
- className={`flex h-6 w-6 cursor-pointer items-center justify-center rounded-md border transition-colors ${
- isCompleted
- ? 'border-green-500 bg-green-500 text-white'
- : 'border-gray-300 bg-white hover:border-purple-400'
- }`}
- >
+ <button
+ type="button"
+ role="checkbox"
+ aria-checked={isCompleted}
+ onClick={onToggle}
+ disabled={!isInteractive}
+ className={`flex h-6 w-6 items-center justify-center rounded-md border transition-colors disabled:opacity-100 disabled:cursor-default ${
+ isInteractive ? 'cursor-pointer' : 'cursor-default'
+ } ${
+ isCompleted
+ ? 'border-green-500 bg-green-500 text-white'
+ : 'border-gray-300 bg-white'
+ } ${isInteractive ? 'hover:border-purple-400' : ''}`}
+ >
{isCompleted && <Check size={16} strokeWidth={3} />}
- </div>
+ </button>🤖 Prompt for AI Agents
In `@components/plans/TaskItem.tsx` around lines 4 - 26, The checkbox-looking
element in TaskItem is visually interactive but has no accessibility or toggle
behavior; update TaskItem to accept an optional onToggle prop on TaskItemProps
and replace or enhance the checkbox div (the element rendering the Check icon
when isCompleted is true) to be an interactive control: use a semantic
interactive element (e.g., button) or add role="checkbox", aria-checked based on
isCompleted, tabIndex, and keyboard handling (toggle on Space/Enter) that calls
onToggle, and only show cursor-pointer when onToggle is provided; also include
an accessible label (aria-label or aria-labelledby) for the control so screen
readers can announce it.
1) 작업한 이슈번호
#38
2) 변경 요약 (What & Why)
3) 스크린샷/동영상 (UI 변경 시)
4) 상세 변경사항
5) 참고사항
Summary by CodeRabbit
릴리스 노트
New Features
Style
✏️ Tip: You can customize this high-level summary in your review settings.