Skip to content

Commit a958739

Browse files
committed
feat: GSAP 애니메이션 적용
1 parent 56c6b73 commit a958739

5 files changed

Lines changed: 149 additions & 11 deletions

File tree

animations/landingGSAP.ts

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ export function initHeroSectionAnimation({
2020
}: HeroSectionRefs) {
2121
ensurePlugins();
2222

23-
// gsap.context를 쓰면 cleanup이 아주 깔끔해짐
2423
const ctx = gsap.context(() => {
2524
gsap.fromTo(
2625
subTextEl,
@@ -32,12 +31,103 @@ export function initHeroSectionAnimation({
3231
ease: 'power2.out',
3332
scrollTrigger: {
3433
trigger: titleEl,
35-
start: 'right bottom+=100 60%',
34+
start: 'top center-=150',
3635
toggleActions: 'play none none reverse',
36+
// markers: true,
3737
},
3838
}
3939
);
4040
});
4141

4242
return () => ctx.revert(); // ✅ cleanup 반환
4343
}
44+
export type FeatureListRefs = {
45+
titleEl: HTMLHeadingElement; // "개발자 성장을 위한"
46+
subTextEl: HTMLDivElement; // 서브 텍스트 wrapper
47+
cardsContainerEl: HTMLDivElement; // grid wrapper
48+
};
49+
50+
export function initFeatureListAnimation({
51+
titleEl,
52+
subTextEl,
53+
cardsContainerEl,
54+
}: FeatureListRefs) {
55+
ensurePlugins();
56+
57+
const ctx = gsap.context(() => {
58+
// 카드들: grid 안의 직계 자식(FeatureCard wrapper)을 전부 잡음
59+
const cards = Array.from(cardsContainerEl.children) as HTMLElement[];
60+
61+
// 초기 상태 (깜빡임 방지)
62+
gsap.set(subTextEl, { opacity: 0, y: 24 });
63+
gsap.set(cards, { opacity: 0, y: 28 });
64+
65+
const tl = gsap.timeline({
66+
scrollTrigger: {
67+
trigger: titleEl,
68+
start: 'top center-=50', // 타이틀 끝이 화면 중간보다 조금 아래에 걸리면
69+
toggleActions: 'play none none reverse',
70+
// markers: true,
71+
},
72+
});
73+
74+
// 1) 서브 텍스트 먼저
75+
tl.to(subTextEl, {
76+
opacity: 1,
77+
y: 0,
78+
duration: 0.6,
79+
ease: 'power2.out',
80+
});
81+
82+
// 2) 카드들 stagger로 아래→위 쓱~
83+
tl.to(
84+
cards,
85+
{
86+
opacity: 1,
87+
y: 0,
88+
duration: 0.6,
89+
ease: 'power2.out',
90+
stagger: 0.12,
91+
},
92+
'-=0.15' // 서브텍스트 끝나기 조금 전에 겹쳐서 시작
93+
);
94+
});
95+
96+
return () => ctx.revert();
97+
}
98+
export type LandingPreviewRefs = {
99+
rootEl: HTMLElement; // 섹션 전체
100+
titleEl: HTMLHeadingElement; // "체계적인 학습으로" h1
101+
contentContainerEl: HTMLDivElement; // FeatureContent 감싸는 div
102+
};
103+
104+
export function initLandingPreviewAnimation({
105+
rootEl,
106+
titleEl,
107+
contentContainerEl,
108+
}: LandingPreviewRefs) {
109+
ensurePlugins();
110+
111+
const ctx = gsap.context(() => {
112+
const items = contentContainerEl.querySelectorAll<HTMLElement>('.fc-item');
113+
114+
// ✅ 레이아웃 유지한 채로 숨김 (grid 비율 절대 안 깨짐)
115+
gsap.set(items, { opacity: 0, y: 24 });
116+
117+
gsap.to(items, {
118+
opacity: 1,
119+
y: 0,
120+
duration: 0.6,
121+
ease: 'power2.out',
122+
stagger: 0.12,
123+
scrollTrigger: {
124+
trigger: titleEl,
125+
start: 'top center-=50', // 필요하면 75%~85%로 튜닝
126+
toggleActions: 'play none none reverse',
127+
// markers: true,
128+
},
129+
});
130+
}, rootEl);
131+
132+
return () => ctx.revert();
133+
}

components/landing/FeatureContent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const FeatureContent = () => {
2323
return (
2424
<ul className="space-y-6">
2525
{ITEMS.map((item) => (
26-
<li key={item.title}>
26+
<li key={item.title} className="fc-item">
2727
<div className="flex gap-3">
2828
<FiCheckCircle className="text-primary text-2xl" />
2929
<p className="text-xl font-medium">{item.title}</p>

components/landing/FeatureList.tsx

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1+
'use client';
2+
13
import FeatureCard from '@/components/landing/FeatureCard';
4+
import { useLayoutEffect, useRef } from 'react';
5+
import { initFeatureListAnimation } from '@/animations/landingGSAP';
6+
27
import {
38
FiBookOpen,
49
FiTarget,
510
FiCheckCircle,
611
FiTrendingUp,
712
} from 'react-icons/fi';
13+
814
const FEATURES = [
915
{
1016
icon: <FiBookOpen className="text-xl" />,
@@ -32,14 +38,34 @@ const FEATURES = [
3238
},
3339
];
3440
const FeatureList = () => {
41+
const titleRef = useRef<HTMLHeadingElement>(null);
42+
const subTextRef = useRef<HTMLDivElement>(null);
43+
const gridRef = useRef<HTMLDivElement>(null);
44+
45+
useLayoutEffect(() => {
46+
if (!titleRef.current || !subTextRef.current || !gridRef.current) return;
47+
48+
const cleanup = initFeatureListAnimation({
49+
titleEl: titleRef.current,
50+
subTextEl: subTextRef.current,
51+
cardsContainerEl: gridRef.current,
52+
});
53+
54+
return cleanup;
55+
}, []);
3556
return (
36-
<div className="flex h-full flex-col items-center justify-center space-y-3 text-center">
37-
<h1 className="text-4xl font-bold">개발자 성장을 위한</h1>
57+
<div className="flex h-full flex-col items-center justify-center space-y-3 pt-100 text-center">
58+
<h1 ref={titleRef} className="text-4xl font-bold">
59+
개발자 성장을 위한
60+
</h1>
3861
<h1 className="text-primary text-4xl font-bold">완벽한 도구</h1>
39-
<div className="text-xl font-light">
62+
<div ref={subTextRef} className="text-xl font-light">
4063
<h2>체계적인 학습과 지속적인 성장을 돕는 강력한 기능들</h2>
4164
</div>
42-
<div className="mt-15 grid max-w-7xl grid-cols-1 gap-6 px-20 sm:grid-cols-2 lg:grid-cols-4">
65+
<div
66+
ref={gridRef}
67+
className="mt-15 grid max-w-7xl grid-cols-1 gap-6 px-20 sm:grid-cols-2 lg:grid-cols-4"
68+
>
4369
{FEATURES.map((feature) => (
4470
<FeatureCard key={feature.title} {...feature} />
4571
))}

components/landing/LandingHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Link from 'next/link';
22

33
const LandingHeader = () => {
44
return (
5-
<div className="fixed flex w-full items-center justify-between bg-white">
5+
<div className="fixed z-50 flex w-full items-center justify-between bg-white">
66
<h1 className="flex items-center gap-2 px-10 py-2 text-3xl font-bold">
77
<span className="text-primary">&lt;/&gt;</span>
88
<span>DevFlow</span>

components/landing/LandingPreview.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,36 @@
1+
'use client';
2+
13
import Image from 'next/image';
24
import FeatureContent from '@/components/landing/FeatureContent';
5+
import { useLayoutEffect, useRef } from 'react';
6+
import { initLandingPreviewAnimation } from '@/animations/landingGSAP';
37

48
const LandingPreview = () => {
9+
const rootRef = useRef<HTMLDivElement>(null);
10+
const titleRef = useRef<HTMLHeadingElement>(null);
11+
const contentRef = useRef<HTMLDivElement>(null);
12+
13+
useLayoutEffect(() => {
14+
if (!rootRef.current || !titleRef.current || !contentRef.current) return;
15+
16+
return initLandingPreviewAnimation({
17+
rootEl: rootRef.current,
18+
titleEl: titleRef.current,
19+
contentContainerEl: contentRef.current,
20+
});
21+
}, []);
522
return (
6-
<div className="mt-30 flex h-full flex-col items-center justify-center space-y-3 pb-50 text-center">
7-
<h1 className="text-4xl font-bold">체계적인 학습으로</h1>
23+
<div
24+
ref={rootRef}
25+
className="mt-30 flex h-full flex-col items-center justify-center space-y-3 pt-50 pb-50 text-center"
26+
>
27+
<h1 ref={titleRef} className="text-4xl font-bold">
28+
체계적인 학습으로
29+
</h1>
830
<h1 className="text-primary text-4xl font-bold">더 빠른 성장</h1>
931
<div className="mx-20 mt-10 grid max-w-7xl grid-cols-1 items-start gap-10 lg:grid-cols-2 lg:gap-2">
1032
{/* 왼쪽: 400 밑으로 못 내려가게 */}
11-
<div className="flex min-w-[355px] justify-center">
33+
<div ref={contentRef} className="flex min-w-[355px] justify-center">
1234
<FeatureContent />
1335
</div>
1436

0 commit comments

Comments
 (0)