Skip to content

Commit b862cf9

Browse files
committed
feat: mobile 기능 소개 애니메이션 기존 코드로 다시 수정
1 parent 994d487 commit b862cf9

5 files changed

Lines changed: 180 additions & 36 deletions

File tree

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,51 @@
1-
import { motion } from "framer-motion";
1+
import { useEffect, useRef, useState } from "react";
2+
import { motion, useScroll, useTransform } from "framer-motion";
3+
import "../../styles/snap.modules.sass";
24

35
import Phone1 from "./Phone1";
46
import Phone2 from "./Phone2";
57
import Phone3 from "./Phone3";
68

7-
import "../../styles/snap.modules.sass";
8-
99
const Component2Mobile = () => {
10-
const fadeInUp = {
11-
initial: { opacity: 0, y: 40 },
12-
animate: { opacity: 1, y: 0 },
13-
transition: { duration: 0.6, ease: "easeOut" },
14-
};
10+
const [activeSection, setActiveSection] = useState(0);
11+
const ref = useRef<HTMLDivElement>(null);
12+
13+
const { scrollYProgress } = useScroll({
14+
target: ref,
15+
offset: ["start end", "end start"],
16+
});
17+
const opacity = useTransform(scrollYProgress, [0.1, 0.12], [0, 1]);
18+
19+
useEffect(() => {
20+
const handleScroll = () => {
21+
if (!ref.current || typeof ref === "function") return;
22+
23+
const sectionTop = ref.current.offsetTop;
24+
const scrollY = window.scrollY;
25+
const sectionHeight = window.innerHeight * 0.5;
26+
27+
const currentSection = Math.floor(
28+
(scrollY - sectionTop + sectionHeight / 2) / sectionHeight
29+
);
30+
31+
setActiveSection(currentSection);
32+
};
33+
34+
window.addEventListener("scroll", handleScroll);
35+
return () => {
36+
window.removeEventListener("scroll", handleScroll);
37+
};
38+
}, [ref]);
1539

1640
return (
17-
<div className="snap-container">
41+
<motion.div
42+
className="mobile-img-cont"
43+
ref={ref}
44+
style={{
45+
opacity,
46+
position: "sticky",
47+
}}
48+
>
1849
<div className="mobile-func-box">
1950
<h1 className="mobile-title2">
2051
4년간의 현장 경험으로 탄생한 솔루션 <br />
@@ -23,24 +54,14 @@ const Component2Mobile = () => {
2354
<h4 className="mobile-sub-title2">
2455
방탈출에만 몰입할 수 있는 특별한 기능을 제공합니다.
2556
</h4>
26-
</div>
2757

28-
<div className="snap-section">
29-
<motion.div {...fadeInUp}>
30-
<Phone1 />
31-
</motion.div>
32-
</div>
33-
<div className="snap-section">
34-
<motion.div {...fadeInUp}>
35-
<Phone2 />
36-
</motion.div>
37-
</div>
38-
<div className="snap-section">
39-
<motion.div {...fadeInUp}>
40-
<Phone3 />
41-
</motion.div>
58+
<div className="mobile-func-img-box">
59+
{activeSection <= 0 && <Phone1 key="phone1" />}
60+
{activeSection === 1 && <Phone2 key="phone2" />}
61+
{activeSection >= 2 && <Phone3 key="phone3" />}
62+
</div>
4263
</div>
43-
</div>
64+
</motion.div>
4465
);
4566
};
4667
export default Component2Mobile;
Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import { forwardRef, useEffect } from "react";
2+
import { useAnimation, motion } from "framer-motion";
13
import Image from "next/image";
24
import "@/(shared)/utils/firebase";
35

46
import useAnalytics from "../../../(shared)/hooks/useAnalytics";
57

6-
const Phone2 = () => {
8+
const Phone2 = forwardRef<HTMLDivElement>((_, ref) => {
79
const { logEvent } = useAnalytics();
810
logEvent("screen_view", {
911
firebase_screen: "homepage_function_2",
@@ -15,16 +17,61 @@ const Phone2 = () => {
1517
width: 180,
1618
height: 350,
1719
};
20+
21+
const controls = useAnimation();
22+
useEffect(() => {
23+
const updateOpacity = () => {
24+
if (typeof ref !== "function" && ref?.current) {
25+
const viewportHeight = window.innerHeight;
26+
27+
const start = ref.current.offsetTop + viewportHeight * 0.7;
28+
const end = ref.current.offsetTop + viewportHeight * 1.4;
29+
30+
const { scrollY } = window;
31+
32+
if (scrollY > start && scrollY < end) {
33+
const progress = (scrollY - start) / (end - start);
34+
const opacity = 1 - progress;
35+
controls.start({ opacity: Math.max(0, opacity) });
36+
} else if (scrollY <= start) {
37+
controls.start({ opacity: 1 });
38+
} else if (scrollY >= end) {
39+
controls.start({ opacity: 0 });
40+
}
41+
}
42+
};
43+
44+
window.addEventListener("scroll", updateOpacity);
45+
updateOpacity();
46+
return () => {
47+
window.removeEventListener("scroll", updateOpacity);
48+
};
49+
}, [controls, ref]);
50+
51+
const phoneVariants = {
52+
hidden: { y: 50, opacity: 0 },
53+
visible: { y: 0, opacity: 1 },
54+
exit: { y: -50, opacity: 0 },
55+
};
56+
1857
return (
19-
<>
58+
<motion.div
59+
className="mobile-img-cont"
60+
ref={ref}
61+
variants={phoneVariants}
62+
initial="hidden"
63+
animate="visible"
64+
exit="exit"
65+
transition={{ duration: 0.5 }}
66+
>
2067
<Image {...imgProps} />
2168
<div className="mobile-func-description">
2269
어두운 공간에서 진행하더라도
2370
<br />
2471
밝은 빛으로 인한 방해가 없어요.
2572
</div>
26-
</>
73+
</motion.div>
2774
);
28-
};
75+
});
2976

3077
export default Phone2;
Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,77 @@
1+
import { useAnimation, motion } from "framer-motion";
2+
import { forwardRef, useEffect } from "react";
13
import Image from "next/image";
24
import "@/(shared)/utils/firebase";
35

46
import useAnalytics from "../../../(shared)/hooks/useAnalytics";
57

6-
const Phone3 = () => {
8+
const Phone3 = forwardRef<HTMLDivElement>((_, ref) => {
79
const { logEvent } = useAnalytics();
810
logEvent("screen_view", {
911
firebase_screen: "homepage_function_3",
1012
firebase_screen_class: "homepage_function_3",
1113
});
14+
1215
const imgProps = {
1316
src: "/images/landing/hint_phone3.svg",
1417
alt: "NEXT ROOM",
1518
width: 180,
1619
height: 350,
1720
};
1821

22+
const controls = useAnimation();
23+
useEffect(() => {
24+
const updateOpacity = () => {
25+
if (typeof ref !== "function" && ref?.current) {
26+
const viewportHeight = window.innerHeight;
27+
28+
const start = ref.current.offsetTop + viewportHeight * 0.7;
29+
const end = ref.current.offsetTop + viewportHeight * 1.8;
30+
31+
const { scrollY } = window;
32+
33+
if (scrollY > start && scrollY < end) {
34+
const progress = (scrollY - start) / (end - start);
35+
const opacity = 1 - progress;
36+
controls.start({ opacity: Math.max(0, opacity) });
37+
} else if (scrollY <= start) {
38+
controls.start({ opacity: 1 });
39+
} else if (scrollY >= end) {
40+
controls.start({ opacity: 0 });
41+
}
42+
}
43+
};
44+
window.addEventListener("scroll", updateOpacity);
45+
updateOpacity(); // Call once to set initial value
46+
47+
return () => {
48+
window.removeEventListener("scroll", updateOpacity);
49+
};
50+
}, [controls, ref]);
51+
52+
const phoneVariants = {
53+
hidden: { y: 50, opacity: 0 },
54+
visible: { y: 0, opacity: 1 },
55+
exit: { y: -50, opacity: 0 },
56+
};
1957
return (
20-
<>
58+
<motion.div
59+
className="mobile-img-cont"
60+
ref={ref}
61+
variants={phoneVariants}
62+
initial="hidden"
63+
animate="visible"
64+
exit="exit"
65+
transition={{ duration: 0.5 }}
66+
>
2167
<Image {...imgProps} />
2268
<div className="mobile-func-description">
2369
각 테마의 독특한 분위기로
2470
<br />
2571
배경화면을 커스텀해보세요.
2672
</div>
27-
</>
73+
</motion.div>
2874
);
29-
};
75+
});
3076

3177
export default Phone3;

app/landing/styles/landing.modules.sass

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,8 @@
327327
&-main3
328328
text-align: center
329329
padding: 80px 0
330+
@media (max-width: 767px)
331+
padding: 200px 0
330332

331333
img
332334
@media (max-width: 767px)
@@ -577,6 +579,8 @@
577579
justify-content: center
578580
align-items: center
579581
gap: 20px
582+
@media (max-width: 767px)
583+
flex-direction: column
580584

581585
&-plan-box
582586
width: 316px

app/landing/styles/snap.modules.sass

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
.mobile
2+
&-wrapper7
3+
height: fit-content
4+
margin: 106px 0
5+
text-align: center
6+
// position: relative
7+
position: sticky
8+
top: 100px
9+
210
&-title2
311
font-size: 20px
412
font-weight: 600
@@ -15,32 +23,50 @@
1523
text-align: center
1624
margin: 10px 0 40px
1725

26+
&-img-cont
27+
position: relative
28+
height: 260vh
29+
1830
&-func-box
1931
position: sticky
20-
top: -2px
32+
top: 100px
2133
display: flex
2234
flex-direction: column
2335
align-items: center
2436
justify-content: flex-start
2537
background-color: black
2638

39+
&-func-title2
40+
position: sticky
41+
font-size: 20px
42+
line-height: 28px
43+
text-align: center
44+
margin-bottom: 20px
45+
2746
&-func-description
2847
font-size: 14px
2948
margin-top: 20px
3049
text-align: center
3150

3251

52+
3353
.snap-container
34-
height: 100vh
3554
overflow-y: scroll
3655
scroll-snap-type: y mandatory
3756
scroll-behavior: smooth
3857
-webkit-overflow-scrolling: touch
39-
position: relative
4058

4159
.snap-section
4260
scroll-snap-align: start
4361
height: 100vh
4462
display: flex
4563
justify-content: center
4664
align-items: center
65+
66+
.sticky-title
67+
position: sticky
68+
top: 0
69+
z-index: 10
70+
background: white
71+
padding: 20px 0
72+
text-align: center

0 commit comments

Comments
 (0)