Skip to content

Commit 6ff687a

Browse files
Merge pull request #30 from DeveloperBlog-Devflow/feature/home-page
feat: 홈 페이지 계획 추가 & 계획 완료 토글 기능 구현
2 parents 27f0bf4 + b956099 commit 6ff687a

5 files changed

Lines changed: 215 additions & 47 deletions

File tree

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

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,99 @@
1+
'use client';
2+
13
import BottomSection from '@/components/home/BottomSection';
24
import GraphSection from '@/components/home/GraphSection';
35
import HeaderSection from '@/components/home/HeaderSection';
46
import ProfileSection from '@/components/home/ProfileSection';
57
import ButtonSection from '@/components/home/ButtonSection';
68

9+
import { useEffect, useState } from 'react';
10+
11+
import {
12+
Todo,
13+
fetchTodos,
14+
AddTodo,
15+
toggleTodoStatus,
16+
} from '@/lib/home/todoService';
17+
18+
import { User, onAuthStateChanged } from 'firebase/auth';
19+
import { auth } from '@/lib/firebase';
20+
721
const Page = () => {
22+
const [todos, setTodos] = useState<Todo[]>([]);
23+
const [error, setError] = useState<string | null>(null);
24+
25+
// 현재 사용자 정보
26+
const [currentUser, setCurrentUser] = useState<User | null>(null);
27+
28+
// 사용자 로그인 상태 감지
29+
useEffect(() => {
30+
const unsubscribe = onAuthStateChanged(auth, (user) => {
31+
setCurrentUser(user);
32+
});
33+
34+
return () => unsubscribe();
35+
}, []);
36+
37+
// 사용자가 존재하면 데이터 불러옴
38+
useEffect(() => {
39+
if (currentUser) {
40+
loadTodos(currentUser.uid);
41+
} else {
42+
setTodos([]);
43+
}
44+
}, [currentUser]);
45+
46+
// 1. 할 일 목록 불러오기
47+
const loadTodos = async (uid: string) => {
48+
try {
49+
setError(null);
50+
51+
const fetchedTodos = await fetchTodos(uid);
52+
setTodos(fetchedTodos);
53+
} catch (err) {
54+
console.error(err);
55+
setError('데이터를 불러오는 데 실패하였습니다');
56+
}
57+
};
58+
59+
// 2. 할 일 추가
60+
const handleAddTodo = async (text: string) => {
61+
if (!currentUser) return;
62+
63+
try {
64+
setError(null);
65+
66+
await AddTodo(currentUser.uid, text);
67+
await loadTodos(currentUser.uid);
68+
} catch (err) {
69+
console.error(err);
70+
setError('할 일을 추가하는 데 실패하였습니다');
71+
}
72+
};
73+
74+
// 3. 할 일 상태 토글
75+
const handleToggleTodo = async (id: string, currentStatus: boolean) => {
76+
if (!currentUser) return;
77+
78+
try {
79+
setError(null);
80+
81+
await toggleTodoStatus(currentUser.uid, id, currentStatus);
82+
await loadTodos(currentUser.uid);
83+
} catch (err) {
84+
console.error(err);
85+
setError('상태를 업데이트하는 데 실패하였습니다');
86+
}
87+
};
88+
89+
if (!currentUser) {
90+
return (
91+
<div className="flex min-h-screen items-center justify-center">
92+
<p>로그인이 필요합니다.</p>
93+
</div>
94+
);
95+
}
96+
897
return (
998
<div className="bg-background flex min-h-screen flex-col gap-4 font-sans md:p-[137px]">
1099
{/* 1. Header */}
@@ -17,10 +106,14 @@ const Page = () => {
17106
<GraphSection></GraphSection>
18107

19108
{/* 2-3. BottomSection */}
20-
<BottomSection className="grid grid-cols-1 gap-4 md:grid-cols-2" />
109+
<BottomSection
110+
className="grid grid-cols-1 gap-4 md:grid-cols-2"
111+
todos={todos}
112+
onToggleTodo={handleToggleTodo}
113+
/>
21114

22115
{/* 3. ButtonSection */}
23-
<ButtonSection />
116+
<ButtonSection onAddTodo={handleAddTodo} />
24117
</div>
25118
);
26119
};

components/home/BottomSection.tsx

Lines changed: 42 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,60 +2,67 @@
22

33
import { useState } from 'react';
44
import Card from './Card';
5-
import CheckList, { ChecklistItem } from './CheckList';
5+
import CheckList from './CheckList';
6+
import { Todo } from '@/lib/home/todoService';
67

78
interface BottomSectionProps {
89
className?: string;
10+
todos: Todo[];
11+
onToggleTodo: (id: string, currentStatus: boolean) => void;
912
}
1013

11-
const TODAY_DUMMY = [
12-
{ id: 't1', text: 'React Hooks 정리', isChecked: false },
13-
{ id: 't2', text: 'GSAP ScrollTrigger 복습', isChecked: true },
14-
{ id: 't3', text: '알고리즘 1문제 풀기', isChecked: false },
15-
];
16-
17-
const UPCOMING_DUMMY = [
18-
{ id: 'u1', text: 'Next.js App Router 정리', isChecked: false },
19-
{ id: 'u2', text: '포트폴리오 리팩토링', isChecked: 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) =>
29-
it.id === id ? { ...it, isChecked: !it.isChecked } : it
30-
)
31-
);
32-
};
33-
34-
const toggleUpcoming = (id: string) => {
35-
setUpcoming((prev) =>
36-
prev.map((it) =>
37-
it.id === id ? { ...it, isChecked: !it.isChecked } : it
38-
)
39-
);
40-
};
14+
// const TODAY_DUMMY = [
15+
// { id: 't1', text: 'React Hooks 정리', isChecked: false },
16+
// { id: 't2', text: 'GSAP ScrollTrigger 복습', isChecked: true },
17+
// { id: 't3', text: '알고리즘 1문제 풀기', isChecked: false },
18+
// ];
19+
20+
// const UPCOMING_DUMMY = [
21+
// { id: 'u1', text: 'Next.js App Router 정리', isChecked: false },
22+
// { id: 'u2', text: '포트폴리오 리팩토링', isChecked: false },
23+
// ];
24+
25+
export default function BottomSection({
26+
className,
27+
todos,
28+
onToggleTodo,
29+
}: BottomSectionProps) {
30+
// const [today, setToday] = useState<ChecklistItem[]>(TODAY_DUMMY);
31+
// const [upcoming, setUpcoming] = useState<ChecklistItem[]>(UPCOMING_DUMMY);
32+
33+
// const toggleToday = (id: string) => {
34+
// setToday((prev) =>
35+
// prev.map((it) =>
36+
// it.id === id ? { ...it, isChecked: !it.isChecked } : it
37+
// )
38+
// );
39+
// };
40+
41+
// const toggleUpcoming = (id: string) => {
42+
// setUpcoming((prev) =>
43+
// prev.map((it) =>
44+
// it.id === id ? { ...it, isChecked: !it.isChecked } : it
45+
// )
46+
// );
47+
// };
4148

4249
return (
4350
<div className={className}>
4451
<Card title="오늘 할 일">
4552
<CheckList
46-
items={today}
47-
onToggle={toggleToday}
53+
items={todos}
54+
onToggleTodo={onToggleTodo}
4855
emptyText="오늘 할 일이 없습니다"
4956
/>
5057
</Card>
5158

52-
<Card title="다가오는 일정">
59+
{/* <Card title="다가오는 일정">
5360
<CheckList
5461
items={upcoming}
5562
onToggle={toggleUpcoming}
5663
emptyText="다가오는 일정이 없습니다"
5764
/>
58-
</Card>
65+
</Card> */}
5966
</div>
6067
);
6168
}

components/home/ButtonSection.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
1-
const ButtonSection = () => {
1+
interface ButtonSectionProps {
2+
onAddTodo: (text: string) => void;
3+
}
4+
5+
const ButtonSection = ({ onAddTodo }: ButtonSectionProps) => {
6+
const handleAddClick = () => {
7+
const text = prompt('할 일을 입력하세요');
8+
if (text) {
9+
onAddTodo(text);
10+
}
11+
};
12+
213
return (
314
<div className="flex w-full gap-6">
415
<button
@@ -10,6 +21,7 @@ const ButtonSection = () => {
1021

1122
<button
1223
type="button"
24+
onClick={handleAddClick}
1325
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"
1426
>
1527
계획 추가하기

components/home/CheckList.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
'use client';
22

3+
import { Todo } from '@/lib/home/todoService';
34
import { CheckItem } from './CheckItem';
45

5-
export type ChecklistItem = {
6-
id: string;
7-
text: string;
8-
isChecked: boolean;
9-
};
6+
// export type ChecklistItem = {
7+
// id: string;
8+
// text: string;
9+
// isChecked: boolean;
10+
// };
1011

1112
interface CheckListProps {
12-
items: ChecklistItem[];
13-
onToggle: (id: string) => void;
13+
items: Todo[];
14+
onToggleTodo: (id: string, currentStatus: boolean) => void;
1415
emptyText?: string;
1516
}
1617

1718
export default function CheckList({
1819
items,
19-
onToggle,
20+
onToggleTodo,
2021
emptyText = '아직 항목이 없습니다',
2122
}: CheckListProps) {
2223
if (items.length === 0) {
@@ -30,7 +31,9 @@ export default function CheckList({
3031
key={item.id}
3132
checked={item.isChecked}
3233
text={item.text}
33-
onToggle={() => onToggle(item.id)}
34+
onToggle={() => {
35+
onToggleTodo(item.id, item.isChecked);
36+
}}
3437
/>
3538
))}
3639
</div>

lib/home/todoService.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import {
2+
addDoc,
3+
collection,
4+
doc,
5+
getDocs,
6+
orderBy,
7+
query,
8+
Timestamp,
9+
updateDoc,
10+
} from 'firebase/firestore';
11+
import { db } from '../firebase';
12+
13+
// 할 일 데이터 타입
14+
export interface Todo {
15+
id: string;
16+
text: string;
17+
isChecked: boolean;
18+
createAt: Date;
19+
}
20+
21+
// 1. 할 일 목록 가져오기
22+
export const fetchTodos = async (uid: string): Promise<Todo[]> => {
23+
const todosCollectionPath = collection(db, 'users', uid, 'todos');
24+
const q = query(todosCollectionPath, orderBy('createAt', 'desc'));
25+
const snapshot = await getDocs(q);
26+
27+
return snapshot.docs.map((doc) => ({
28+
id: doc.id,
29+
...doc.data(),
30+
createAt: doc.data().createAt.toDate() ?? new Date(),
31+
})) as Todo[];
32+
};
33+
34+
// 2. 할 일 추가하기
35+
export const AddTodo = async (uid: string, text: string) => {
36+
await addDoc(collection(db, 'users', uid, 'todos'), {
37+
text,
38+
isChecked: false,
39+
createAt: Timestamp.now(),
40+
});
41+
};
42+
43+
// 3. 완료 상태 토글
44+
export const toggleTodoStatus = async (
45+
uid: string,
46+
todoId: string,
47+
currentStatus: boolean
48+
) => {
49+
const todoRef = doc(db, 'users', uid, 'todos', todoId);
50+
await updateDoc(todoRef, {
51+
isChecked: !currentStatus,
52+
});
53+
};

0 commit comments

Comments
 (0)