Skip to content

Commit d804be2

Browse files
committed
feat: 오늘의 할일 체크리스트 구현
1 parent eac18df commit d804be2

6 files changed

Lines changed: 151 additions & 36 deletions

File tree

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,7 @@ const Page = () => {
104104
{/* 2-3. BottomSection */}
105105
<BottomSection
106106
className="grid grid-cols-1 gap-4 md:grid-cols-2"
107-
todos={todos}
108-
onToggleTodo={handleToggleTodo}
107+
uid={currentUser.uid}
109108
/>
110109

111110
{/* 3. ButtonSection */}

components/home/BottomSection.tsx

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

3-
import { useState } from 'react';
4-
import Card from './Card';
5-
import CheckList from './CheckList';
6-
import { Todo } from '@/services/home/todoService.service';
7-
3+
import TodayPlanContainer from './TodayPlanContainer';
84
interface BottomSectionProps {
5+
uid: string;
96
className?: string;
10-
todos: Todo[];
11-
onToggleTodo: (id: string, currentStatus: boolean) => void;
127
}
138

149
// const TODAY_DUMMY = [
@@ -22,11 +17,7 @@ interface BottomSectionProps {
2217
// { id: 'u2', text: '포트폴리오 리팩토링', isChecked: false },
2318
// ];
2419

25-
export default function BottomSection({
26-
className,
27-
todos,
28-
onToggleTodo,
29-
}: BottomSectionProps) {
20+
export default function BottomSection({ className, uid }: BottomSectionProps) {
3021
// const [today, setToday] = useState<ChecklistItem[]>(TODAY_DUMMY);
3122
// const [upcoming, setUpcoming] = useState<ChecklistItem[]>(UPCOMING_DUMMY);
3223

@@ -48,13 +39,7 @@ export default function BottomSection({
4839

4940
return (
5041
<div className={className}>
51-
<Card title="오늘 할 일">
52-
<CheckList
53-
items={todos}
54-
onToggleTodo={onToggleTodo}
55-
emptyText="오늘 할 일이 없습니다"
56-
/>
57-
</Card>
42+
<TodayPlanContainer uid={uid}></TodayPlanContainer>
5843

5944
{/* <Card title="다가오는 일정">
6045
<CheckList

components/home/CheckList.tsx

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

3-
import { Todo } from '@/services/home/todoService.service';
43
import { CheckItem } from './CheckItem';
54

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

12-
interface CheckListProps {
13-
items: Todo[];
14-
onToggleTodo: (id: string, currentStatus: boolean) => void;
11+
type CheckListProps<T extends CheckableItem> = {
12+
items: T[];
13+
onToggleTodo: (id: string, checked: boolean) => void;
1514
emptyText?: string;
16-
}
15+
};
1716

18-
export default function CheckList({
17+
export default function CheckList<T extends CheckableItem>({
1918
items,
2019
onToggleTodo,
2120
emptyText = '아직 항목이 없습니다',
22-
}: CheckListProps) {
21+
}: CheckListProps<T>) {
2322
if (items.length === 0) {
2423
return <p className="text-sm text-gray-400">{emptyText}</p>;
2524
}
@@ -31,9 +30,7 @@ export default function CheckList({
3130
key={item.id}
3231
checked={item.isChecked}
3332
text={item.text}
34-
onToggle={() => {
35-
onToggleTodo(item.id, item.isChecked);
36-
}}
33+
onToggle={() => onToggleTodo(item.id, item.isChecked)}
3734
/>
3835
))}
3936
</div>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
'use client';
2+
3+
import Card from '@/components/home/Card';
4+
import CheckList from '@/components/home/CheckList';
5+
import { useTodayPlanItems } from '@/hooks/useTodayPlanItems';
6+
7+
export default function TodayPlanContainer({ uid }: { uid: string }) {
8+
const { items, loading, error, toggle } = useTodayPlanItems(uid);
9+
10+
if (loading) {
11+
return <div className="text-sm text-gray-400">불러오는 중...</div>;
12+
}
13+
14+
if (error) {
15+
return <div className="text-sm text-red-500">{error}</div>;
16+
}
17+
18+
return (
19+
<Card title="오늘의 할 일">
20+
<CheckList
21+
items={items}
22+
onToggleTodo={(id, checked) => toggle(id, checked)}
23+
emptyText="오늘 완료할 계획이 없습니다"
24+
/>
25+
</Card>
26+
);
27+
}

hooks/useTodayPlanItems.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// hooks/useTodayPlanItems.ts
2+
import { useCallback, useEffect, useMemo, useState } from 'react';
3+
import type { PlanItem } from '@/services/plans/planManageService.service';
4+
import {
5+
fetchTodayPlanItems,
6+
toggleItemStatus,
7+
} from '@/services/plans/planManageService.service';
8+
9+
export const useTodayPlanItems = (uid?: string) => {
10+
const [items, setItems] = useState<PlanItem[]>([]);
11+
const [loading, setLoading] = useState(false);
12+
const [error, setError] = useState<string | null>(null);
13+
14+
const load = useCallback(async () => {
15+
if (!uid) return;
16+
try {
17+
setLoading(true);
18+
setError(null);
19+
const data = await fetchTodayPlanItems(uid);
20+
setItems(data);
21+
} catch (e) {
22+
console.error(e);
23+
setError('오늘의 계획을 불러오는 데 실패했습니다.');
24+
} finally {
25+
setLoading(false);
26+
}
27+
}, [uid]);
28+
29+
// ✅ 진행률 계산
30+
const total = useMemo(() => items.length, [items]);
31+
const completed = useMemo(
32+
() => items.reduce((acc, it) => acc + (it.isChecked ? 1 : 0), 0),
33+
[items]
34+
);
35+
const progressText = useMemo(
36+
() => `${completed}/${total}`,
37+
[completed, total]
38+
);
39+
40+
// ✅ 토글만
41+
const toggle = useCallback(
42+
async (id: string, current: boolean) => {
43+
if (!uid) return;
44+
setError(null);
45+
46+
// optimistic update
47+
setItems((prev) =>
48+
prev.map((it) => (it.id === id ? { ...it, isChecked: !current } : it))
49+
);
50+
51+
try {
52+
await toggleItemStatus(uid, id, current);
53+
} catch (e) {
54+
console.error(e);
55+
// rollback
56+
setItems((prev) =>
57+
prev.map((it) => (it.id === id ? { ...it, isChecked: current } : it))
58+
);
59+
setError('상태 변경에 실패했습니다.');
60+
}
61+
},
62+
[uid]
63+
);
64+
65+
useEffect(() => {
66+
load();
67+
}, [load]);
68+
69+
return {
70+
items,
71+
loading,
72+
error,
73+
toggle,
74+
progressText,
75+
};
76+
};

services/plans/planManageService.service.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,3 +186,34 @@ export const updatePlanItem = async (
186186
await updateDoc(itemRef, updatePayload);
187187
}
188188
};
189+
190+
export const fetchTodayPlanItems = async (uid: string): Promise<PlanItem[]> => {
191+
const itemsRef = collection(db, 'users', uid, 'planItems');
192+
193+
// KST 기준 오늘 00:00 ~ 내일 00:00
194+
const now = new Date();
195+
const kst = new Date(now.getTime() + 9 * 60 * 60 * 1000);
196+
const startKST = new Date(kst.getFullYear(), kst.getMonth(), kst.getDate());
197+
const start = new Date(startKST.getTime() - 9 * 60 * 60 * 1000);
198+
const end = new Date(start.getTime() + 24 * 60 * 60 * 1000);
199+
200+
const q = query(
201+
itemsRef,
202+
where('deadline', '>=', Timestamp.fromDate(start)),
203+
where('deadline', '<', Timestamp.fromDate(end)),
204+
orderBy('deadline', 'asc'),
205+
orderBy('createdAt', 'asc')
206+
);
207+
208+
const snapshot = await getDocs(q);
209+
210+
return snapshot.docs.map((docSnap) => {
211+
const data = docSnap.data();
212+
return {
213+
id: docSnap.id,
214+
...data,
215+
createdAt: data.createdAt?.toDate(),
216+
deadline: data.deadline?.toDate(),
217+
};
218+
}) as PlanItem[];
219+
};

0 commit comments

Comments
 (0)