Skip to content

Commit 2909ef3

Browse files
authored
Merge branch 'dev' into feature/docs-page
2 parents 71c0edb + 3d58855 commit 2909ef3

18 files changed

Lines changed: 558 additions & 97 deletions

animations/landingGSAP.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { gsap } from 'gsap';
2+
import { ScrollTrigger } from 'gsap/ScrollTrigger';
3+
4+
let pluginsRegistered = false;
5+
6+
interface AnimateOptions {
7+
trigger: HTMLElement; // 스크롤 트리거 기준 요소
8+
targets: gsap.TweenTarget; // 애니메이션 대상
9+
stagger?: number; // 순차 재생 간격
10+
start?: string; // 스크롤 시작 지점
11+
yOffset?: number; // y축 이동 거리
12+
}
13+
14+
/* Fade Up 함수 */
15+
const animateFadeUp = ({
16+
trigger,
17+
targets,
18+
stagger = 0,
19+
start = 'top center-=50',
20+
yOffset = 24,
21+
}: AnimateOptions) => {
22+
if (!pluginsRegistered) {
23+
gsap.registerPlugin(ScrollTrigger);
24+
pluginsRegistered = true;
25+
}
26+
27+
const ctx = gsap.context(() => {
28+
gsap.fromTo(
29+
targets,
30+
{ opacity: 0, y: yOffset },
31+
{
32+
opacity: 1,
33+
y: 0,
34+
duration: 0.6,
35+
ease: 'power2.out',
36+
stagger: stagger,
37+
scrollTrigger: {
38+
trigger: trigger,
39+
start: start,
40+
toggleActions: 'play none none reverse',
41+
// markers: true,
42+
},
43+
}
44+
);
45+
});
46+
47+
return () => ctx.revert();
48+
};
49+
50+
/* --- 1. Hero Section --- */
51+
export type HeroSectionRefs = {
52+
titleEl: HTMLHeadingElement;
53+
subTextEl: HTMLDivElement;
54+
};
55+
56+
export const initHeroSectionAnimation = ({
57+
titleEl,
58+
subTextEl,
59+
}: HeroSectionRefs) => {
60+
return animateFadeUp({
61+
trigger: titleEl,
62+
targets: subTextEl,
63+
start: 'top center-=150',
64+
yOffset: 40,
65+
});
66+
};
67+
68+
/* --- 2. Feature List Section --- */
69+
export type FeatureListRefs = {
70+
titleEl: HTMLHeadingElement;
71+
subTextEl: HTMLDivElement;
72+
cardsContainerEl: HTMLDivElement;
73+
};
74+
75+
export const initFeatureListAnimation = ({
76+
titleEl,
77+
subTextEl,
78+
cardsContainerEl,
79+
}: FeatureListRefs) => {
80+
const cards = Array.from(cardsContainerEl.children);
81+
82+
return animateFadeUp({
83+
trigger: titleEl,
84+
targets: [subTextEl, ...cards],
85+
stagger: 0.12,
86+
});
87+
};
88+
89+
/* --- 3. Landing Preview Section --- */
90+
export type LandingPreviewRefs = {
91+
titleEl: HTMLHeadingElement;
92+
contentContainerEl: HTMLDivElement;
93+
};
94+
95+
export const initLandingPreviewAnimation = ({
96+
titleEl,
97+
contentContainerEl,
98+
}: LandingPreviewRefs) => {
99+
const items = contentContainerEl.querySelectorAll('.fc-item');
100+
101+
return animateFadeUp({
102+
trigger: titleEl,
103+
targets: items,
104+
stagger: 0.2,
105+
});
106+
};

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

Lines changed: 116 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,139 @@
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+
import { getProfile, Profile } from '@/lib/home/profileService';
18+
19+
import { User, onAuthStateChanged } from 'firebase/auth';
20+
import { auth } from '@/lib/firebase';
21+
722
const Page = () => {
23+
const [profile, setProfile] = useState<Profile | null>(null);
24+
const [todos, setTodos] = useState<Todo[]>([]);
25+
26+
const [error, setError] = useState<string | null>(null);
27+
28+
// 현재 사용자 정보
29+
const [currentUser, setCurrentUser] = useState<User | null>(null);
30+
31+
// 1. 할 일 목록 불러오기
32+
const loadTodos = async (uid: string) => {
33+
try {
34+
setError(null);
35+
36+
const fetchedTodos = await fetchTodos(uid);
37+
setTodos(fetchedTodos);
38+
} catch (err) {
39+
console.error(err);
40+
setError('데이터를 불러오는 데 실패하였습니다');
41+
}
42+
};
43+
44+
// 2. 할 일 추가
45+
const handleAddTodo = async (text: string) => {
46+
if (!currentUser) return;
47+
48+
try {
49+
setError(null);
50+
51+
await AddTodo(currentUser.uid, text);
52+
await loadTodos(currentUser.uid);
53+
} catch (err) {
54+
console.error(err);
55+
setError('할 일을 추가하는 데 실패하였습니다');
56+
}
57+
};
58+
59+
// 3. 할 일 상태 토글
60+
const handleToggleTodo = async (id: string, currentStatus: boolean) => {
61+
if (!currentUser) return;
62+
63+
try {
64+
setError(null);
65+
66+
await toggleTodoStatus(currentUser.uid, id, currentStatus);
67+
await loadTodos(currentUser.uid);
68+
} catch (err) {
69+
console.error(err);
70+
setError('상태를 업데이트하는 데 실패하였습니다');
71+
}
72+
};
73+
74+
// 사용자 로그인 상태 감지
75+
useEffect(() => {
76+
const unsubscribe = onAuthStateChanged(auth, (user) => {
77+
setCurrentUser(user);
78+
});
79+
80+
return () => unsubscribe();
81+
}, []);
82+
83+
// 사용자가 존재하면 데이터 불러옴
84+
useEffect(() => {
85+
if (currentUser) {
86+
const loadData = async () => {
87+
try {
88+
const userProfile = await getProfile(currentUser.uid);
89+
setProfile(userProfile);
90+
await loadTodos(currentUser.uid);
91+
} catch (err) {
92+
console.error(err);
93+
setError('프로필 정보를 불러오는 데 실패하였습니다');
94+
}
95+
};
96+
loadData();
97+
} else {
98+
setTodos([]);
99+
setProfile(null);
100+
}
101+
}, [currentUser]);
102+
103+
// 유저 정보가 없을 시
104+
if (!currentUser) {
105+
return (
106+
<div className="flex min-h-screen items-center justify-center">
107+
<p>로그인이 필요합니다.</p>
108+
</div>
109+
);
110+
}
111+
112+
// 유저 정보가 있을 시
8113
return (
9114
<div className="bg-background flex min-h-screen flex-col gap-4 font-sans md:p-[137px]">
10115
{/* 1. Header */}
11116
<HeaderSection />
12117

13118
{/* 2-1. ProfileSection */}
14-
<ProfileSection className="grid grid-cols-1 gap-4 md:grid-cols-3" />
119+
<ProfileSection
120+
className="grid grid-cols-1 gap-4 md:grid-cols-3"
121+
profile={profile}
122+
uid={currentUser.uid}
123+
/>
15124

16125
{/* 2-2. GraphSection */}
17126
<GraphSection></GraphSection>
18127

19128
{/* 2-3. BottomSection */}
20-
<BottomSection className="grid grid-cols-1 gap-4 md:grid-cols-2" />
129+
<BottomSection
130+
className="grid grid-cols-1 gap-4 md:grid-cols-2"
131+
todos={todos}
132+
onToggleTodo={handleToggleTodo}
133+
/>
21134

22135
{/* 3. ButtonSection */}
23-
<ButtonSection />
136+
<ButtonSection onAddTodo={handleAddTodo} />
24137
</div>
25138
);
26139
};

components/auth/LoginForm.tsx

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,10 @@ import { FaGithub } from 'react-icons/fa';
55
import Button from '@/components/auth/Button';
66
import Link from 'next/link';
77
import { auth } from '@/lib/firebase';
8-
import {
9-
GithubAuthProvider,
10-
signInWithEmailAndPassword,
11-
signInWithPopup,
12-
UserCredential,
13-
} from 'firebase/auth';
8+
import { signInWithEmailAndPassword } from 'firebase/auth';
149
import { useRouter } from 'next/navigation';
1510
import { useState } from 'react';
11+
import { signupWithGithub } from '@/services/auth/signup.service';
1612

1713
type LoginFormProps = {
1814
handleOpenModal: () => void;
@@ -26,20 +22,17 @@ const LoginForm = ({ handleOpenModal }: LoginFormProps) => {
2622

2723
const router = useRouter();
2824

29-
const githubProvider = new GithubAuthProvider();
30-
3125
const handleGithubLogin = async () => {
26+
if (isLoading) return;
27+
setIsLoading(true);
28+
setError('');
3229
try {
33-
const result: UserCredential = await signInWithPopup(
34-
auth,
35-
githubProvider
36-
);
37-
const user = result.user;
38-
// console.log('로그인 성공 : ', user);
30+
await signupWithGithub();
3931
router.push('/');
4032
} catch (err: unknown) {
41-
if (err instanceof Error) {
42-
}
33+
setError('Github 로그인에 실패했습니다.');
34+
} finally {
35+
setIsLoading(false);
4336
}
4437
};
4538

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
}

0 commit comments

Comments
 (0)