Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions app/(with-sidebar)/plans/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import FormField from '@/components/auth/FormField';
import PageHeader from '@/components/common/PageHeader';
import Card from '@/components/home/Card';
import AddPlanButton from '@/components/plans/AddPlanButton';
import PlanSection from '@/components/plans/PlanSection';
import SearchBar from '@/components/plans/SearchBar';

import { Target, Calendar, CheckCircle2 } from 'lucide-react';

const sampleTasks = [
{
id: 1,
text: 'useState, useEffect 기초',
date: '2025-01-20',
isChecked: true,
},
{
id: 2,
text: 'useContext, useReducer',
date: '2025-01-22',
isChecked: false,
},
{
id: 3,
text: 'Custom Hooks 만들기',
date: '2025-01-22',
isChecked: false,
},
];

const Page = () => {
return (
<div className="bg-background min-h-screen p-11">
{/* 1. 페이지 헤더 */}
<PageHeader
title="플랜"
highlight="관리하기"
description="학습 주제를 만들고 세부 과제를 관리하세요"
/>

{/* 2. 상단 통계 카드 (Grid) */}
<div className="mb-4 grid grid-cols-1 gap-3.5 md:grid-cols-3">
<Card className="flex items-center justify-between border-2 border-[#D5DCFB]">
{/* 왼쪽: 텍스트 영역 */}
<div className="flex flex-col gap-1">
<span className="text-text-sub text-sm font-medium">
전체 플랜 수
</span>
<span className="text-4xl font-bold text-[#4757D3]">1</span>
</div>

{/* 오른쪽: 아이콘 영역 */}
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[#D5DCFB] text-[#556BD6]">
<Target size={24} strokeWidth={2.5} />
</div>
</Card>
<Card className="flex items-center justify-between border-2 border-[#EBDBFC]">
{/* 왼쪽: 텍스트 영역 */}
<div className="flex flex-col gap-1">
<span className="text-text-sub text-sm font-medium">
진행중인 플랜 수
</span>
<span className="text-4xl font-bold text-[#7B44C4]">1</span>
</div>

{/* 오른쪽: 아이콘 영역 */}
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[#EBDBFC] text-[#7B44C4]">
<Calendar size={24} strokeWidth={2.5} />
</div>
</Card>
<Card className="flex items-center justify-between border-2 border-[#C6F6D7]">
{/* 왼쪽: 텍스트 영역 */}
<div className="flex flex-col gap-1">
<span className="text-text-sub text-sm font-medium">
완료된 플랜 수
</span>
<span className="text-4xl font-bold text-[#00841F]">1</span>
</div>

{/* 오른쪽: 아이콘 영역 */}
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[#C6F6D7] text-[#00841F]">
<CheckCircle2 size={24} strokeWidth={2.5} />
</div>
</Card>
</div>

{/* 3. 검색 바 */}
<SearchBar />

{/* 4. 메인 플랜 목록 */}
<section className="space-y-6">
<PlanSection
title="React Hooks 학습"
description="React Hooks의 기본부터 고급 패턴까지 학습"
tasks={sampleTasks}
/>

{/* 추가적인 PlanSection이 있다면 여기에 배치 */}
</section>

{/* 5. 하단 추가 버튼 */}
<div className="mt-6">
<AddPlanButton />
</div>
</div>
);
};

export default Page;
31 changes: 31 additions & 0 deletions components/common/PageHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
interface PageHeaderProps {
title: string;
highlight?: string;
description?: string;
className?: string;
}

const PageHeader = ({
title,
highlight,
description,
className = '',
}: PageHeaderProps) => {
return (
<div className={`mb-8 pt-12 text-3xl ${className}`}>
<h1 className="text-text text-4xl font-bold tracking-tight">
{title}
{/* highlight가 있을 때만 보라색으로 렌더링 */}
{highlight && <span className="text-primary ml-2">{highlight}</span>}
</h1>

{description && (
<p className="text-text-sub mt-2 text-[1.5rem] font-medium">
{description}
</p>
)}
</div>
);
};

export default PageHeader;
7 changes: 4 additions & 3 deletions components/common/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import { auth } from '@/lib/firebase';
const navItems = [
{ label: '홈', href: '/', icon: Home },
{ label: '개발 일지', href: '/logs', icon: ClipboardList },
{ label: '새 계획 만들기', href: '/plans/new', icon: CalendarPlus },
{ label: '새 페이지 만들기', href: '/write', icon: CopyPlus },
];
{ label: '계획 관리', href: '/plans/new', icon: CalendarPlus },
Copy link
Copy Markdown

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:

#!/bin/bash
# Find all plans-related page files
find . -path "*/plans/*" -name "*.tsx" -o -path "*/plans/*" -name "*.ts" | grep -E "(page|layout)" | head -20

Repository: 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 -30

Repository: 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.tsx

Repository: 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 -20

Repository: 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).

{ label: '새 페이지 만들기', href: '/pages/new', icon: CopyPlus },
]


const Sidebar = () => {
const pathname = usePathname();
Expand Down
4 changes: 1 addition & 3 deletions components/home/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ interface CardProps {

const Card = ({ children, className = '', title }: CardProps) => {
return (
<div
className={`border-border bg-surface rounded-[10px] p-6 shadow-sm ${className}`}
>
<div className={`bg-surface rounded-[10px] p-6 shadow-sm ${className}`}>
{title && (
<h2 className="mb-4 text-lg font-bold text-gray-900">{title}</h2>
)}
Expand Down
13 changes: 13 additions & 0 deletions components/plans/AddPlanButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// components/dashboard/AddPlanButton.tsx
import { Plus } from 'lucide-react';

export default function AddPlanButton() {
return (
<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">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-100 text-blue-600">
<Plus size={20} />
</div>
새로운 플랜 추가하기
</button>
);
}
81 changes: 81 additions & 0 deletions components/plans/PlanSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// components/dashboard/PlanSection.tsx
'use client'; // 상태 관리를 위해 클라이언트 컴포넌트 선언

import { useState } from 'react';
import { ChevronDown, ChevronUp, Plus } from 'lucide-react';
import Card from '../home/Card'; // 기존에 만든 Card 재사용
import TaskItem from './TaskItem';

interface Task {
id: number;
text: string;
date: string;
isChecked: boolean;
}

interface PlanSectionProps {
title: string;
description: string;
tasks: Task[];
}

export default function PlanSection({
title,
description,
tasks,
}: PlanSectionProps) {
const [isOpen, setIsOpen] = useState(true); // 기본적으로 열려있게 설정

// 완료된 할 일 개수 계산
const completedCount = tasks.filter((t) => t.isChecked).length;
const totalCount = tasks.length;

return (
<Card className="mb-4 transition-all duration-200">
{/* 헤더 영역 (클릭 시 토글) */}
<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>
Comment on lines +36 to +55
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

토글 헤더가 비접근성(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.

Suggested change
<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.

</div>

{/* 펼쳐지는 내용 영역 */}
{isOpen && (
<div className="animate-fadeIn mt-6">
{/* 할 일 목록 */}
<div className="flex flex-col gap-2">
{tasks.map((task) => (
<TaskItem
key={task.id}
text={task.text}
date={task.date}
isCompleted={task.isChecked}
/>
))}
</div>

{/* 하위 항목 추가 버튼 (점선) */}
<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} /> 새 하위항목 추가
Comment on lines +74 to +75
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

버튼에 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.

Suggested change
<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.

</button>
</div>
)}
</Card>
);
}
17 changes: 17 additions & 0 deletions components/plans/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// components/dashboard/SearchBar.tsx
import { Search } from 'lucide-react';

export default function SearchBar() {
return (
<div className="mb-4 flex justify-end">
<div className="relative">
<input
type="text"
placeholder="검색어를 입력하세요"
className="w-64 rounded-full border border-gray-200 bg-white py-2 pr-10 pl-4 text-sm shadow-sm focus:border-purple-400 focus:outline-none"
/>
<Search className="absolute top-2.5 right-3 text-gray-400" size={18} />
</div>
</div>
);
}
38 changes: 38 additions & 0 deletions components/plans/TaskItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// components/dashboard/TaskItem.tsx
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

파일 경로 주석이 실제 위치와 불일치합니다.
현재 주석이 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.

Suggested change
// 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 주석입니다.

import { Check } from 'lucide-react';

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>
Comment on lines +4 to +26
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

체크박스가 클릭 가능해 보이지만 동작·접근성이 없습니다.
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.


<div className="flex flex-col">
<span
className={`text-sm font-medium ${isCompleted ? 'text-gray-400 line-through' : 'text-gray-700'}`}
>
{text}
</span>
<span className="text-xs text-gray-400">마감: {date}</span>
</div>
</div>
);
}