Skip to content

Commit 8d824c2

Browse files
authored
Merge pull request #56 from DeveloperBlog-Devflow/feature/home-page-plan
feat: 홈페이지 오늘 할 일/다가오는 일정 표시
2 parents eac18df + 043f09f commit 8d824c2

10 files changed

Lines changed: 383 additions & 127 deletions

File tree

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

Lines changed: 16 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,49 +8,23 @@ import ButtonSection from '@/components/home/ButtonSection';
88

99
import { useEffect, useState } from 'react';
1010
import { useAuthUser } from '@/hooks/useAuthUser';
11-
12-
import {
13-
Todo,
14-
fetchTodos,
15-
AddTodo,
16-
toggleTodoStatus,
17-
} from '@/services/home/todoService.service';
11+
import { useTodayPlanItems } from '@/hooks/useTodayPlanItems';
1812
import { getProfile, Profile } from '@/services/home/profileService.service';
1913

2014
const Page = () => {
2115
// 현재 사용자 정보
2216
const { user: currentUser, authLoading } = useAuthUser();
23-
2417
const [profile, setProfile] = useState<Profile | null>(null);
25-
const [todos, setTodos] = useState<Todo[]>([]);
2618
const [error, setError] = useState<string | null>(null);
2719

28-
// 1. 할 일 목록 불러오기
29-
const loadTodos = async (uid: string) => {
30-
try {
31-
setError(null);
32-
33-
const fetchedTodos = await fetchTodos(uid);
34-
setTodos(fetchedTodos);
35-
} catch (err) {
36-
console.error(err);
37-
setError('데이터를 불러오는 데 실패하였습니다');
38-
}
39-
};
40-
41-
// 3. 할 일 상태 토글
42-
const handleToggleTodo = async (id: string, currentStatus: boolean) => {
43-
if (!currentUser) return;
44-
try {
45-
setError(null);
46-
47-
await toggleTodoStatus(currentUser.uid, id, currentStatus);
48-
await loadTodos(currentUser.uid);
49-
} catch (err) {
50-
console.error(err);
51-
setError('상태를 업데이트하는 데 실패하였습니다');
52-
}
53-
};
20+
const {
21+
items,
22+
loading: planLoading,
23+
error: planError,
24+
toggle,
25+
progressText,
26+
total,
27+
} = useTodayPlanItems(currentUser?.uid);
5428

5529
// 사용자가 존재하면 데이터 불러옴
5630
useEffect(() => {
@@ -59,7 +33,6 @@ const Page = () => {
5933
try {
6034
const userProfile = await getProfile(currentUser.uid);
6135
setProfile(userProfile);
62-
await loadTodos(currentUser.uid);
6336
} catch (err) {
6437
console.error(err);
6538
setError('프로필 정보를 불러오는 데 실패하였습니다');
@@ -96,16 +69,21 @@ const Page = () => {
9669
className="grid grid-cols-1 gap-4 md:grid-cols-3"
9770
profile={profile}
9871
uid={currentUser.uid}
72+
progressText={progressText}
9973
/>
10074

10175
{/* 2-2. GraphSection */}
10276
<GraphSection uid={currentUser.uid}></GraphSection>
10377

10478
{/* 2-3. BottomSection */}
10579
<BottomSection
80+
uid={currentUser.uid}
10681
className="grid grid-cols-1 gap-4 md:grid-cols-2"
107-
todos={todos}
108-
onToggleTodo={handleToggleTodo}
82+
items={items}
83+
loading={planLoading}
84+
error={planError}
85+
onToggle={toggle}
86+
todoTotal={total}
10987
/>
11088

11189
{/* 3. ButtonSection */}

components/home/BottomSection.tsx

Lines changed: 32 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,48 @@
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';
3+
import TodayPlanContainer from './TodayPlanContainer';
4+
import UpcomingPlanContainer from '@/components/home/UpcomingPlanContainer';
5+
import type { PlanItem } from '@/services/plans/planManageService.service';
6+
import { useUpcomingPlanItems } from '@/hooks/useUpcomingPlanItems';
77

88
interface BottomSectionProps {
9+
uid: string;
910
className?: string;
10-
todos: Todo[];
11-
onToggleTodo: (id: string, currentStatus: boolean) => void;
11+
items: PlanItem[];
12+
loading: boolean;
13+
error: string | null;
14+
onToggle: (id: string, checked: boolean) => void;
15+
todoTotal: number;
1216
}
1317

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-
2518
export default function BottomSection({
19+
uid,
2620
className,
27-
todos,
28-
onToggleTodo,
21+
items,
22+
loading,
23+
error,
24+
onToggle,
25+
todoTotal,
2926
}: 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-
// };
27+
const upcoming = useUpcomingPlanItems(uid);
4828

4929
return (
5030
<div className={className}>
51-
<Card title="오늘 할 일">
52-
<CheckList
53-
items={todos}
54-
onToggleTodo={onToggleTodo}
55-
emptyText="오늘 할 일이 없습니다"
56-
/>
57-
</Card>
58-
59-
{/* <Card title="다가오는 일정">
60-
<CheckList
61-
items={upcoming}
62-
onToggle={toggleUpcoming}
63-
emptyText="다가오는 일정이 없습니다"
64-
/>
65-
</Card> */}
31+
{/* 오늘의 목표 */}
32+
<TodayPlanContainer
33+
items={items}
34+
loading={loading}
35+
error={error}
36+
onToggle={onToggle}
37+
></TodayPlanContainer>
38+
39+
{/* 다가오는 일정 */}
40+
<UpcomingPlanContainer
41+
items={upcoming.items}
42+
loading={upcoming.loading}
43+
error={upcoming.error}
44+
limit={todoTotal}
45+
/>
6646
</div>
6747
);
6848
}

components/home/CheckItem.tsx

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,45 @@ type CheckItemProps = {
66
checked: boolean;
77
text: string;
88
onToggle: () => void;
9+
checkbox?: boolean;
910
};
1011

11-
export function CheckItem({ checked, text, onToggle }: CheckItemProps) {
12+
export function CheckItem({
13+
checked,
14+
text,
15+
onToggle,
16+
checkbox,
17+
}: CheckItemProps) {
1218
return (
13-
<button
14-
type="button"
15-
onClick={onToggle}
19+
<div
20+
role="button"
21+
onClick={checkbox ? onToggle : undefined}
22+
aria-disabled={!checkbox}
1623
className={[
1724
'flex w-full items-center gap-2.5 rounded-2xl border px-4 py-2 text-left transition',
1825
'bg-gray-50 hover:bg-gray-100',
1926
'border-gray-300',
2027
].join(' ')}
2128
>
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+
{checkbox && (
30+
<div
2931
className={[
30-
'transition-all',
31-
checked
32-
? 'scale-100 text-green-500 opacity-100'
33-
: 'scale-50 opacity-0',
32+
'flex h-6 w-6 items-center justify-center rounded-full border-2 transition',
33+
checked ? 'border-green-500' : 'border-gray-400',
3434
].join(' ')}
35-
size={16}
36-
strokeWidth={4}
37-
/>
38-
</div>
35+
>
36+
<Check
37+
className={[
38+
'transition-all',
39+
checked
40+
? 'scale-100 text-green-500 opacity-100'
41+
: 'scale-50 opacity-0',
42+
].join(' ')}
43+
size={16}
44+
strokeWidth={4}
45+
/>
46+
</div>
47+
)}
3948
<span
4049
className={[
4150
'text-sm',
@@ -44,6 +53,6 @@ export function CheckItem({ checked, text, onToggle }: CheckItemProps) {
4453
>
4554
{text}
4655
</span>
47-
</button>
56+
</div>
4857
);
4958
}

components/home/CheckList.tsx

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
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, current: boolean) => void;
1514
emptyText?: string;
16-
}
15+
checkbox?: boolean;
16+
};
1717

18-
export default function CheckList({
18+
export default function CheckList<T extends CheckableItem>({
1919
items,
2020
onToggleTodo,
2121
emptyText = '아직 항목이 없습니다',
22-
}: CheckListProps) {
22+
checkbox,
23+
}: CheckListProps<T>) {
2324
if (items.length === 0) {
2425
return <p className="text-sm text-gray-400">{emptyText}</p>;
2526
}
@@ -31,9 +32,8 @@ export default function CheckList({
3132
key={item.id}
3233
checked={item.isChecked}
3334
text={item.text}
34-
onToggle={() => {
35-
onToggleTodo(item.id, item.isChecked);
36-
}}
35+
onToggle={() => onToggleTodo(item.id, item.isChecked)}
36+
checkbox={checkbox}
3737
/>
3838
))}
3939
</div>

components/home/ProfileSection.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@ interface ProfileSectionProps {
77
className?: string;
88
profile: Profile | null;
99
uid: string;
10+
progressText: string;
1011
}
1112

12-
const ProfileSection = ({ className, profile, uid }: ProfileSectionProps) => {
13+
const ProfileSection = ({
14+
className,
15+
profile,
16+
uid,
17+
progressText,
18+
}: ProfileSectionProps) => {
1319
const icon = getRandomProfileIcon(uid);
1420
const avatarUrl = profile?.avatar_url;
1521

@@ -49,7 +55,9 @@ const ProfileSection = ({ className, profile, uid }: ProfileSectionProps) => {
4955

5056
<div className="md:col-span-1">
5157
<Card className="flex h-full flex-col items-center justify-center text-center">
52-
<div className="text-primary mb-1 text-4xl font-bold">3/5</div>
58+
<div className="text-primary mb-1 text-4xl font-bold">
59+
{progressText}
60+
</div>
5361
<div className="text-sm font-medium text-gray-500">
5462
오늘의 목표 달성률
5563
</div>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use client';
2+
3+
import Card from '@/components/home/Card';
4+
import CheckList from '@/components/home/CheckList';
5+
import { PlanItem } from '@/services/plans/planManageService.service';
6+
7+
type TodayPlanContainerProps = {
8+
items: PlanItem[];
9+
loading: boolean;
10+
error: string | null;
11+
onToggle: (id: string, checked: boolean) => void;
12+
};
13+
14+
export default function TodayPlanContainer({
15+
items,
16+
loading,
17+
error,
18+
onToggle,
19+
}: TodayPlanContainerProps) {
20+
if (loading) {
21+
return <div className="text-sm text-gray-400">불러오는 중...</div>;
22+
}
23+
24+
if (error) {
25+
return <div className="text-sm text-red-500">{error}</div>;
26+
}
27+
28+
return (
29+
<Card title="오늘의 할 일">
30+
<CheckList
31+
items={items}
32+
onToggleTodo={(id, checked) => onToggle(id, checked)}
33+
emptyText="오늘 완료할 계획이 없습니다"
34+
checkbox={true}
35+
/>
36+
</Card>
37+
);
38+
}

0 commit comments

Comments
 (0)