Skip to content

Commit f6c3603

Browse files
committed
feat: 계획 관리 페이지 UI 디자인 구현
1 parent dc7e306 commit f6c3603

8 files changed

Lines changed: 290 additions & 8 deletions

File tree

app/(with-sidebar)/plan/page.tsx

Lines changed: 0 additions & 5 deletions
This file was deleted.

app/(with-sidebar)/plans/page.tsx

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import FormField from '@/components/auth/FormField';
2+
import PageHeader from '@/components/common/PageHeader';
3+
import Card from '@/components/home/Card';
4+
import AddPlanButton from '@/components/plans/AddPlanButton';
5+
import PlanSection from '@/components/plans/PlanSection';
6+
import SearchBar from '@/components/plans/SearchBar';
7+
8+
import { Target, Calendar, CheckCircle2 } from 'lucide-react';
9+
10+
const sampleTasks = [
11+
{
12+
id: 1,
13+
text: 'useState, useEffect 기초',
14+
date: '2025-01-20',
15+
isChecked: true,
16+
},
17+
{
18+
id: 2,
19+
text: 'useContext, useReducer',
20+
date: '2025-01-22',
21+
isChecked: false,
22+
},
23+
{
24+
id: 3,
25+
text: 'Custom Hooks 만들기',
26+
date: '2025-01-22',
27+
isChecked: false,
28+
},
29+
];
30+
31+
const Page = () => {
32+
return (
33+
<div className="bg-background min-h-screen p-11">
34+
{/* 1. 페이지 헤더 */}
35+
<PageHeader
36+
title="플랜"
37+
highlight="관리하기"
38+
description="학습 주제를 만들고 세부 과제를 관리하세요"
39+
/>
40+
41+
{/* 2. 상단 통계 카드 (Grid) */}
42+
<div className="mb-4 grid grid-cols-1 gap-3.5 md:grid-cols-3">
43+
<Card className="flex items-center justify-between border-2 border-[#D5DCFB]">
44+
{/* 왼쪽: 텍스트 영역 */}
45+
<div className="flex flex-col gap-1">
46+
<span className="text-text-sub text-sm font-medium">
47+
전체 플랜 수
48+
</span>
49+
<span className="text-4xl font-bold text-[#4757D3]">1</span>
50+
</div>
51+
52+
{/* 오른쪽: 아이콘 영역 */}
53+
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[#D5DCFB] text-[#556BD6]">
54+
<Target size={24} strokeWidth={2.5} />
55+
</div>
56+
</Card>
57+
<Card className="flex items-center justify-between border-2 border-[#EBDBFC]">
58+
{/* 왼쪽: 텍스트 영역 */}
59+
<div className="flex flex-col gap-1">
60+
<span className="text-text-sub text-sm font-medium">
61+
전체 플랜 수
62+
</span>
63+
<span className="text-4xl font-bold text-[#7B44C4]">1</span>
64+
</div>
65+
66+
{/* 오른쪽: 아이콘 영역 */}
67+
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[#EBDBFC] text-[#7B44C4]">
68+
<Calendar size={24} strokeWidth={2.5} />
69+
</div>
70+
</Card>
71+
<Card className="flex items-center justify-between border-2 border-[#C6F6D7]">
72+
{/* 왼쪽: 텍스트 영역 */}
73+
<div className="flex flex-col gap-1">
74+
<span className="text-text-sub text-sm font-medium">
75+
전체 플랜 수
76+
</span>
77+
<span className="text-4xl font-bold text-[#00841F]">1</span>
78+
</div>
79+
80+
{/* 오른쪽: 아이콘 영역 */}
81+
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[#C6F6D7] text-[#00841F]">
82+
<CheckCircle2 size={24} strokeWidth={2.5} />
83+
</div>
84+
</Card>
85+
</div>
86+
87+
{/* 3. 검색 바 */}
88+
<SearchBar />
89+
90+
{/* 4. 메인 플랜 목록 */}
91+
<section className="space-y-6">
92+
<PlanSection
93+
title="React Hooks 학습"
94+
description="React Hooks의 기본부터 고급 패턴까지 학습"
95+
tasks={sampleTasks}
96+
/>
97+
98+
{/* 추가적인 PlanSection이 있다면 여기에 배치 */}
99+
</section>
100+
101+
{/* 5. 하단 추가 버튼 */}
102+
<div className="mt-6">
103+
<AddPlanButton />
104+
</div>
105+
</div>
106+
);
107+
};
108+
109+
export default Page;

components/common/PageHeader.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
interface PageHeaderProps {
2+
title: string;
3+
highlight?: string;
4+
description?: string;
5+
className?: string;
6+
}
7+
8+
const PageHeader = ({
9+
title,
10+
highlight,
11+
description,
12+
className = '',
13+
}: PageHeaderProps) => {
14+
return (
15+
<div className={`mb-8 pt-12 text-3xl ${className}`}>
16+
<h1 className="text-text text-4xl font-bold tracking-tight">
17+
{title}
18+
{/* highlight가 있을 때만 보라색으로 렌더링 */}
19+
{highlight && <span className="text-primary ml-2">{highlight}</span>}
20+
</h1>
21+
22+
{description && (
23+
<p className="text-text-sub mt-2 text-[1.5rem] font-medium">
24+
{description}
25+
</p>
26+
)}
27+
</div>
28+
);
29+
};
30+
31+
export default PageHeader;

components/home/Card.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@ interface CardProps {
1010

1111
const Card = ({ children, className = '', title }: CardProps) => {
1212
return (
13-
<div
14-
className={`border-border bg-surface rounded-[10px] p-6 shadow-sm ${className}`}
15-
>
13+
<div className={`bg-surface rounded-[10px] p-6 shadow-sm ${className}`}>
1614
{title && (
1715
<h2 className="mb-4 text-lg font-bold text-gray-900">{title}</h2>
1816
)}

components/plans/AddPlanButton.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// components/dashboard/AddPlanButton.tsx
2+
import { Plus } from 'lucide-react';
3+
4+
export default function AddPlanButton() {
5+
return (
6+
<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">
7+
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-100 text-blue-600">
8+
<Plus size={20} />
9+
</div>
10+
새로운 플랜 추가하기
11+
</button>
12+
);
13+
}

components/plans/PlanSection.tsx

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// components/dashboard/PlanSection.tsx
2+
'use client'; // 상태 관리를 위해 클라이언트 컴포넌트 선언
3+
4+
import { useState } from 'react';
5+
import { ChevronDown, ChevronUp, Plus } from 'lucide-react';
6+
import Card from '../home/Card'; // 기존에 만든 Card 재사용
7+
import TaskItem from './TaskItem';
8+
9+
interface Task {
10+
id: number;
11+
text: string;
12+
date: string;
13+
isChecked: boolean;
14+
}
15+
16+
interface PlanSectionProps {
17+
title: string;
18+
description: string;
19+
tasks: Task[];
20+
}
21+
22+
export default function PlanSection({
23+
title,
24+
description,
25+
tasks,
26+
}: PlanSectionProps) {
27+
const [isOpen, setIsOpen] = useState(true); // 기본적으로 열려있게 설정
28+
29+
// 완료된 할 일 개수 계산
30+
const completedCount = tasks.filter((t) => t.isChecked).length;
31+
const totalCount = tasks.length;
32+
33+
return (
34+
<Card className="mb-4 transition-all duration-200">
35+
{/* 헤더 영역 (클릭 시 토글) */}
36+
<div
37+
className="flex cursor-pointer items-start justify-between"
38+
onClick={() => setIsOpen(!isOpen)}
39+
>
40+
<div>
41+
<h3 className="text-xl font-bold text-gray-900">{title}</h3>
42+
<p className="mt-1 text-sm text-gray-500">{description}</p>
43+
</div>
44+
45+
<div className="flex items-center gap-4">
46+
<div className="text-right">
47+
<span className="block text-xs text-gray-500">진행률</span>
48+
<span className="text-lg font-bold text-[#556BD6]">
49+
{completedCount}/{totalCount}
50+
</span>
51+
</div>
52+
<button className="text-gray-400 hover:text-gray-600">
53+
{isOpen ? <ChevronUp size={24} /> : <ChevronDown size={24} />}
54+
</button>
55+
</div>
56+
</div>
57+
58+
{/* 펼쳐지는 내용 영역 */}
59+
{isOpen && (
60+
<div className="animate-fadeIn mt-6">
61+
{/* 할 일 목록 */}
62+
<div className="flex flex-col gap-2">
63+
{tasks.map((task) => (
64+
<TaskItem
65+
key={task.id}
66+
text={task.text}
67+
date={task.date}
68+
isCompleted={task.isChecked}
69+
/>
70+
))}
71+
</div>
72+
73+
{/* 하위 항목 추가 버튼 (점선) */}
74+
<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">
75+
<Plus size={16} /> 새 하위항목 추가
76+
</button>
77+
</div>
78+
)}
79+
</Card>
80+
);
81+
}

components/plans/SearchBar.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// components/dashboard/SearchBar.tsx
2+
import { Search } from 'lucide-react';
3+
4+
export default function SearchBar() {
5+
return (
6+
<div className="mb-4 flex justify-end">
7+
<div className="relative">
8+
<input
9+
type="text"
10+
placeholder="검색어를 입력하세요"
11+
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"
12+
/>
13+
<Search className="absolute top-2.5 right-3 text-gray-400" size={18} />
14+
</div>
15+
</div>
16+
);
17+
}

components/plans/TaskItem.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// components/dashboard/TaskItem.tsx
2+
import { Check } from 'lucide-react';
3+
4+
interface TaskItemProps {
5+
text: string;
6+
date: string;
7+
isCompleted: boolean;
8+
}
9+
10+
export default function TaskItem({ text, date, isCompleted }: TaskItemProps) {
11+
return (
12+
<div
13+
className={`mb-2 flex items-center gap-3 rounded-xl p-4 transition-colors ${
14+
isCompleted ? 'bg-gray-50' : 'bg-gray-100'
15+
}`}
16+
>
17+
{/* 체크박스 커스텀 */}
18+
<div
19+
className={`flex h-6 w-6 cursor-pointer items-center justify-center rounded-md border transition-colors ${
20+
isCompleted
21+
? 'border-green-500 bg-green-500 text-white'
22+
: 'border-gray-300 bg-white hover:border-purple-400'
23+
}`}
24+
>
25+
{isCompleted && <Check size={16} strokeWidth={3} />}
26+
</div>
27+
28+
<div className="flex flex-col">
29+
<span
30+
className={`text-sm font-medium ${isCompleted ? 'text-gray-400 line-through' : 'text-gray-700'}`}
31+
>
32+
{text}
33+
</span>
34+
<span className="text-xs text-gray-400">마감: {date}</span>
35+
</div>
36+
</div>
37+
);
38+
}

0 commit comments

Comments
 (0)