Skip to content

Commit f3622ad

Browse files
authored
Merge pull request #25 from DeveloperBlog-Devflow/feature/home-page
feat: 홈 페이지에서 사용하는 컴포넌트 구현 및 섹션 분리하여 홈 페이지 구현
2 parents 5b8c6c6 + 211302e commit f3622ad

9 files changed

Lines changed: 283 additions & 4 deletions

File tree

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

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
1-
// export default function Home() {
2-
// return <div>home</div>;
3-
// }
1+
import BottomSection from '@/components/home/BottomSection';
2+
import GraphSection from '@/components/home/GraphSection';
3+
import HeaderSection from '@/components/home/HeaderSection';
4+
import ProfileSection from '@/components/home/ProfileSection';
5+
import ButtonSection from '@/components/home/ButtonSection';
46

57
const Page = () => {
6-
return <div>home</div>;
8+
return (
9+
<div className="bg-background flex min-h-screen flex-col gap-6 font-sans md:p-[137px]">
10+
{/* 1. Header */}
11+
<HeaderSection />
12+
13+
{/* 2-1. ProfileSection */}
14+
<ProfileSection className="grid grid-cols-1 gap-6 md:grid-cols-3" />
15+
16+
{/* 2-2. GraphSection */}
17+
<GraphSection></GraphSection>
18+
19+
{/* 2-3. BottomSection */}
20+
<BottomSection className="grid grid-cols-1 gap-6 md:grid-cols-2" />
21+
22+
{/* 3. ButtonSection */}
23+
<ButtonSection />
24+
</div>
25+
);
726
};
827

928
export default Page;

components/home/BottomSection.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use client';
2+
3+
import { useState } from 'react';
4+
import Card from './Card';
5+
import CheckList, { ChecklistItem } from './CheckList';
6+
7+
interface BottomSectionProps {
8+
className?: string;
9+
}
10+
11+
const TODAY_DUMMY = [
12+
{ id: 't1', text: 'React Hooks 정리', checked: false },
13+
{ id: 't2', text: 'GSAP ScrollTrigger 복습', checked: true },
14+
{ id: 't3', text: '알고리즘 1문제 풀기', checked: false },
15+
];
16+
17+
const UPCOMING_DUMMY = [
18+
{ id: 'u1', text: 'Next.js App Router 정리', checked: false },
19+
{ id: 'u2', text: '포트폴리오 리팩토링', checked: false },
20+
];
21+
22+
export default function BottomSection({ className }: BottomSectionProps) {
23+
const [today, setToday] = useState<ChecklistItem[]>(TODAY_DUMMY);
24+
const [upcoming, setUpcoming] = useState<ChecklistItem[]>(UPCOMING_DUMMY);
25+
26+
const toggleToday = (id: string) => {
27+
setToday((prev) =>
28+
prev.map((it) => (it.id === id ? { ...it, checked: !it.checked } : it))
29+
);
30+
};
31+
32+
const toggleUpcoming = (id: string) => {
33+
setUpcoming((prev) =>
34+
prev.map((it) => (it.id === id ? { ...it, checked: !it.checked } : it))
35+
);
36+
};
37+
38+
return (
39+
<div className={className}>
40+
<Card title="오늘 할 일">
41+
<CheckList
42+
items={today}
43+
onToggle={toggleToday}
44+
emptyText="오늘 할 일이 없습니다"
45+
/>
46+
</Card>
47+
48+
<Card title="다가오는 일정">
49+
<CheckList
50+
items={upcoming}
51+
onToggle={toggleUpcoming}
52+
emptyText="다가오는 일정이 없습니다"
53+
/>
54+
</Card>
55+
</div>
56+
);
57+
}

components/home/ButtonSection.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const ButtonSection = () => {
2+
return (
3+
<div className="flex w-full max-w-5xl gap-6">
4+
<button
5+
type="button"
6+
className="bg-primary flex-1 rounded-xl py-4 text-center text-base font-semibold text-white shadow-sm transition hover:bg-violet-500 active:bg-violet-700 disabled:cursor-not-allowed disabled:opacity-60"
7+
>
8+
새 TIL 작성
9+
</button>
10+
11+
<button
12+
type="button"
13+
className="flex-1 rounded-xl border border-slate-300 bg-none py-4 text-center text-base font-semibold text-slate-700 transition hover:bg-slate-200 active:bg-slate-300 disabled:cursor-not-allowed disabled:opacity-60"
14+
>
15+
계획 추가하기
16+
</button>
17+
</div>
18+
);
19+
};
20+
21+
export default ButtonSection;

components/home/Card.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ReactNode } from 'react';
2+
3+
// 흰 바탕의 카드
4+
5+
interface CardProps {
6+
children: ReactNode; // 카드 내부 내용들
7+
className?: string; // 추가 스타일링
8+
title?: string; // 좌측 상단 제목이 있는 경우
9+
}
10+
11+
const Card = ({ children, className = '', title }: CardProps) => {
12+
return (
13+
<div
14+
className={`border-border bg-surface rounded-[10px] p-6 shadow-sm ${className}`}
15+
>
16+
{title && (
17+
<h2 className="mb-4 text-lg font-bold text-gray-900">{title}</h2>
18+
)}
19+
{children}
20+
</div>
21+
);
22+
};
23+
24+
export default Card;

components/home/CheckItem.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
'use client';
2+
3+
import { Check } from 'lucide-react';
4+
5+
type CheckItemProps = {
6+
checked: boolean;
7+
text: string;
8+
onToggle: () => void;
9+
};
10+
11+
export function CheckItem({ checked, text, onToggle }: CheckItemProps) {
12+
return (
13+
<button
14+
type="button"
15+
onClick={onToggle}
16+
className={[
17+
'flex w-full items-center gap-4 rounded-2xl border px-5 py-2 text-left transition',
18+
'bg-gray-50 hover:bg-gray-100',
19+
'border-gray-300',
20+
].join(' ')}
21+
>
22+
<div
23+
className={[
24+
'flex h-6 w-6 items-center justify-center rounded-full border-2 transition',
25+
checked ? 'border-green-500' : 'border-gray-400',
26+
].join(' ')}
27+
>
28+
<Check
29+
className={[
30+
'transition-all',
31+
checked
32+
? 'scale-100 text-green-500 opacity-100'
33+
: 'scale-50 opacity-0',
34+
].join(' ')}
35+
size={16}
36+
strokeWidth={4}
37+
/>
38+
</div>
39+
<span
40+
className={[
41+
'text-sm',
42+
checked ? 'text-gray-400 line-through' : 'text-gray-800',
43+
].join(' ')}
44+
>
45+
{text}
46+
</span>
47+
</button>
48+
);
49+
}

components/home/CheckList.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use client';
2+
3+
import { CheckItem } from './CheckItem';
4+
5+
export type ChecklistItem = {
6+
id: string;
7+
text: string;
8+
checked: boolean;
9+
};
10+
11+
interface CheckListProps {
12+
items: ChecklistItem[];
13+
onToggle: (id: string) => void;
14+
emptyText?: string;
15+
}
16+
17+
export default function CheckList({
18+
items,
19+
onToggle,
20+
emptyText = '아직 항목이 없습니다',
21+
}: CheckListProps) {
22+
if (items.length === 0) {
23+
return <p className="text-sm text-gray-400">{emptyText}</p>;
24+
}
25+
26+
return (
27+
<div className="space-y-3">
28+
{items.map((item) => (
29+
<CheckItem
30+
key={item.id}
31+
checked={item.checked}
32+
text={item.text}
33+
onToggle={() => onToggle(item.id)}
34+
/>
35+
))}
36+
</div>
37+
);
38+
}

components/home/GraphSection.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import Card from './Card';
2+
3+
interface GraphSectionProps {
4+
className?: string;
5+
}
6+
7+
const GraphSection = ({ className }: GraphSectionProps) => {
8+
return (
9+
<div className={className}>
10+
<Card title="학습 기록">잔디그래프 구현 예정</Card>
11+
</div>
12+
);
13+
};
14+
15+
export default GraphSection;

components/home/HeaderSection.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
interface HeaderSectionProps {
2+
className?: string;
3+
}
4+
5+
const HeaderSection = ({ className }: HeaderSectionProps) => {
6+
return (
7+
<div className={className}>
8+
<h1 className="flex items-center gap-2 text-3xl font-bold text-gray-900">
9+
안녕하세요 <span className="text-3xl">🖐️</span>
10+
</h1>
11+
<p className="mt-1 text-gray-500">오늘도 성장하는 하루를 만들어보세요!</p>
12+
</div>
13+
);
14+
};
15+
16+
export default HeaderSection;

components/home/ProfileSection.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import Card from './Card';
2+
3+
interface ProfileSectionProps {
4+
className?: string;
5+
}
6+
7+
const ProfileSection = ({ className }: ProfileSectionProps) => {
8+
return (
9+
<div className={className}>
10+
<div className="md:col-span-2">
11+
<Card className="flex items-center gap-4">
12+
<div className="flex h-16 w-16 items-center justify-center overflow-hidden rounded-full border border-gray-100 bg-purple-100">
13+
<span className="text-3xl">😈</span>
14+
</div>
15+
16+
<div>
17+
<h3 className="text-xl font-bold text-gray-900">이건무</h3>
18+
<p className="text-sm text-gray-500">@mnmnnmm324</p>
19+
<div className="mt-1 flex gap-2 text-xs font-medium">
20+
<span className="text-primary">38193일 </span>연속
21+
<span className="text-gray-400">|</span>
22+
<span className="text-primary">12개 </span>TIL
23+
</div>
24+
</div>
25+
</Card>
26+
</div>
27+
28+
<div className="md:col-span-1">
29+
<Card className="flex h-full flex-col items-center justify-center text-center">
30+
<div className="text-primary mb-1 text-4xl font-bold">3/5</div>
31+
<div className="text-sm font-medium text-gray-500">
32+
오늘의 목표 달성률
33+
</div>
34+
</Card>
35+
</div>
36+
</div>
37+
);
38+
};
39+
40+
export default ProfileSection;

0 commit comments

Comments
 (0)