Skip to content

Commit 699fa09

Browse files
authored
fix: 온보딩 애니메이션 오류 해결 (#41)
- 온보딩 편지 애니메이션 모바일 오류 해결 - 온보딩 단계에 따른 상태관리 - 온보딩이 끝난 후 홈으로 이동 Co-authored-by: nirii00 <[email protected]>
1 parent f8358f8 commit 699fa09

File tree

7 files changed

+205
-80
lines changed

7 files changed

+205
-80
lines changed

src/pages/Onboarding/SetZipCode.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { useEffect, useState } from 'react';
2+
13
import Spinner from './components/Spinner';
24

35
const SetZipCode = ({
@@ -6,7 +8,13 @@ const SetZipCode = ({
68
setIsZipCodeSet: React.Dispatch<React.SetStateAction<boolean>>;
79
}) => {
810
const DUMMY_ZIPCODE = '122A2';
11+
const [isBtnActive, setIsBtnActive] = useState<boolean>(false);
912

13+
useEffect(() => {
14+
setTimeout(() => {
15+
setIsBtnActive(true);
16+
}, 6300);
17+
}, []);
1018
return (
1119
<>
1220
<header className="flex flex-col items-center">
@@ -16,13 +24,16 @@ const SetZipCode = ({
1624
</header>
1725
<section className="flex gap-2">
1826
{DUMMY_ZIPCODE.split('').map((char, index) => (
19-
<Spinner key={index} target={`${char}`} index={index}></Spinner>
27+
<Spinner key={index} target={char} index={index}></Spinner>
2028
))}
2129
</section>
2230
<button
2331
type="button"
32+
disabled={!isBtnActive}
2433
className="primary-btn body-m w-full py-2"
25-
onClick={() => setIsZipCodeSet(true)}
34+
onClick={() => {
35+
setIsZipCodeSet(true);
36+
}}
2637
>
2738
다음으로
2839
</button>
Lines changed: 77 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,39 @@
11
import { useState, useRef, useEffect } from 'react';
2-
import { Link } from 'react-router';
32
import { twMerge } from 'tailwind-merge';
43

54
import envelope from '@/assets/images/closed-letter.png';
6-
import envelopeTop from '@/assets/images/envelope-pink-back-top.png';
5+
// import envelopeTop from '@/assets/images/envelope-pink-back-top.png';
76
import envelopeFront from '@/assets/images/opened-letter-front.png';
87

9-
export default function UserInteraction() {
8+
export default function UserInteraction({
9+
setIsAnimationOver,
10+
}: {
11+
setIsAnimationOver: React.Dispatch<React.SetStateAction<boolean>>;
12+
}) {
1013
const imgRef = useRef<HTMLImageElement>(null);
11-
const [imgPos, setImgPos] = useState<{ top: number; width: number }>({ top: 0, width: 0 });
14+
const [imgPos, setImgPos] = useState<{
15+
top: number;
16+
width: number;
17+
height: number;
18+
left: number;
19+
}>({
20+
top: 0,
21+
width: 0,
22+
height: 0,
23+
left: 0,
24+
});
1225
const [imgToBottom, setImgToBottom] = useState<boolean>(false);
1326

1427
const [startAnimation, setStartAnimation] = useState<boolean>(false);
15-
const [openAnimation, setOpenAnimation] = useState<boolean>(false);
28+
// const [openAnimation, setOpenAnimation] = useState<boolean>(false);
1629
const [letterOutAnimation, setLetterOutAnimation] = useState<boolean>(false);
1730
const [envelopeOut, setEnvelopeOut] = useState<boolean>(false);
1831
const [finishAnimation, setFinishAnimation] = useState<boolean>(false);
1932

2033
const handleLetterClick = () => {
2134
if (imgRef.current) {
2235
const rect = imgRef.current.getBoundingClientRect();
23-
setImgPos({ top: rect.top, width: rect.width });
36+
setImgPos({ top: rect.top, width: rect.width, height: rect.height, left: rect.left });
2437
}
2538
setStartAnimation(true);
2639
setTimeout(() => {
@@ -31,18 +44,18 @@ export default function UserInteraction() {
3144
useEffect(() => {
3245
if (imgToBottom) {
3346
setTimeout(() => {
34-
setOpenAnimation(true);
47+
setLetterOutAnimation(true);
3548
}, 1000);
3649
}
3750
}, [imgToBottom]);
3851

39-
useEffect(() => {
40-
if (openAnimation) {
41-
setTimeout(() => {
42-
setLetterOutAnimation(true);
43-
}, 2000);
44-
}
45-
}, [openAnimation]);
52+
// useEffect(() => {
53+
// if (openAnimation) {
54+
// setTimeout(() => {
55+
// setLetterOutAnimation(true);
56+
// }, 2000);
57+
// }
58+
// }, [openAnimation]);
4659

4760
useEffect(() => {
4861
if (letterOutAnimation) {
@@ -56,9 +69,18 @@ export default function UserInteraction() {
5669
if (envelopeOut) {
5770
setTimeout(() => {
5871
setFinishAnimation(true);
59-
}, 2000);
72+
}, 1000);
6073
}
6174
}, [envelopeOut]);
75+
76+
useEffect(() => {
77+
if (finishAnimation) {
78+
setTimeout(() => {
79+
setIsAnimationOver(true);
80+
}, 2000);
81+
}
82+
}, [finishAnimation]);
83+
6284
if (startAnimation === false) {
6385
return (
6486
<>
@@ -80,67 +102,78 @@ export default function UserInteraction() {
80102
);
81103
} else {
82104
return (
83-
<>
105+
<div className="relative h-[calc(100vh-110px)] w-full overflow-hidden">
84106
<img
85107
src={envelopeFront}
86-
alt="분홍색 편지지"
108+
alt=""
87109
className={twMerge(
88-
`z-30 mx-10 h-auto rounded transition-transform duration-1000 ease-in-out`,
89-
imgToBottom && 'translate-y-full',
110+
`transform-translation z-30 mx-10 h-auto rounded`,
111+
imgToBottom && !envelopeOut && 'animate-envelopeSink',
90112
envelopeOut && 'animate-envelopeOut',
91113
)}
92114
style={{
93-
top: `${imgPos.top}px`,
115+
top: `calc(${imgPos.top}px - 5rem)`,
94116
position: 'absolute',
95117
width: `${imgPos.width}px`,
118+
left: '0px',
96119
}}
97120
/>
98-
{letterOutAnimation && (
99-
<div
100-
className="animate-expandScale to-gray-5 z-20 max-w-[600px] rounded-lg bg-linear-to-b from-white"
101-
style={{
102-
width: `${imgPos.width - imgPos.width * 0.1}px`,
103-
bottom: `${imgPos.top - 0.7 * imgPos.top}px`,
104-
top: `${imgPos.top - 0.5 * imgPos.top}px`,
105-
position: 'absolute',
106-
}}
107-
></div>
108-
)}
109-
{openAnimation && (
121+
{/* {openAnimation && (
110122
<img
111123
src={envelopeTop}
112124
alt=""
113125
className={twMerge(
114-
`z-10 mx-10 h-auto rounded transition-transform duration-1000 ease-in-out`,
115-
imgToBottom && 'translate-y-full',
116-
openAnimation && 'animate-openEnvelope',
126+
`z-10 mx-10 h-auto rounded`,
127+
openAnimation && !envelopeOut && 'animate-openEnvelope',
117128
envelopeOut && 'animate-envelopeOut',
118129
)}
119130
style={{
120-
top: `${imgPos.top}px`,
131+
bottom: `calc(${sinkRefTop.top}px - 7.5rem)`,
121132
position: 'absolute',
122133
width: `${imgPos.width}px`,
123-
transformOrigin: 'bottom',
134+
left: '0px',
124135
}}
125136
/>
126-
)}
137+
)} */}
127138
<img
128139
src={envelope}
129140
alt="분홍색 편지지"
141+
// ref={sinkRef}
142+
// onAnimationEnd={(e) => {
143+
// if (e.animationName === 'envelopeSink') {
144+
// const rect = sinkRef.current?.getBoundingClientRect();
145+
// if (rect?.top) setSinkRefTop({ top: rect?.top });
146+
// console.log('Animation ended. boundingRect top:', rect?.top);
147+
// }
148+
// }}
130149
className={twMerge(
131-
`z-0 mx-10 h-auto rounded transition-transform duration-1000 ease-in-out`,
132-
imgToBottom && 'translate-y-full',
150+
`z-0 mx-10 h-auto rounded`,
151+
imgToBottom && !envelopeOut && 'animate-envelopeSink',
133152
envelopeOut && 'animate-envelopeOut',
134153
)}
135154
style={{
136-
top: `${imgPos.top}px`,
155+
top: `calc(${imgPos.top}px - 5rem)`,
137156
position: 'absolute',
138157
width: `${imgPos.width}px`,
158+
left: '0px',
139159
}}
140160
/>
141-
{/* TODO: 편지지 링크 */}
142-
{finishAnimation && <Link to={'/'}></Link>}
143-
</>
161+
{letterOutAnimation && (
162+
<div
163+
className={twMerge(
164+
'letter-gradient z-20 max-w-[600px] rounded-lg',
165+
finishAnimation ? 'animate-fadeOut' : 'animate-expandScale',
166+
)}
167+
style={{
168+
width: `${imgPos.width - imgPos.width * 0.1}px`,
169+
bottom: `${0.9 * imgPos.height}px`,
170+
top: `${imgPos.top - 0.7 * imgPos.top}px`,
171+
position: 'absolute',
172+
left: `58px`,
173+
}}
174+
></div>
175+
)}
176+
</div>
144177
);
145178
}
146179
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { useNavigate } from 'react-router';
2+
3+
export default function index() {
4+
// eslint-disable-next-line react-hooks/rules-of-hooks
5+
const navigate = useNavigate();
6+
return (
7+
<main className="animate-fadeIn absolute inset-0 flex h-full w-full flex-col justify-end bg-white px-5 pt-7.5 pb-4 opacity-0">
8+
<article className="basic-theme mt-7.5 mb-9 grow pl-4">
9+
<h1 className="font-malang mt-15">To.따숨이</h1>
10+
<h2 className="font-malang">환영합니다! 우리 함께 마음을 나누어 보아요</h2>
11+
<section className="mt-9" style={{ fontFamily: 'KyoboHandwriting2020A' }}>
12+
<p>안녕하세요, 따숨이님!</p>
13+
<br />
14+
<p>요즘 어떤 말을 하고싶으신가요?</p>
15+
<p>36.5에서 따뜻한 마음의 편지를 나누어 보세요.</p>
16+
<br />
17+
<p>따뜻한 편지 문화를 위해 아래의 안내 사항을 숙지해주세요!</p>
18+
<p>1. 욕설, 비방, 성희롱은 금지입니다.</p>
19+
<p>
20+
2. 만약 위의 이유로 신고를 당할 경우 경고를 받게 되고, 세번의 경고를 받게 되면 서비스를
21+
이용하실 수 없습니다.
22+
</p>
23+
<p>3. 고민 편지에 대한 답장은 검수 후에 전달됩니다.</p>
24+
</section>
25+
<p className="font-malang mt-22">From.9황작물</p>
26+
</article>
27+
<button
28+
className="primary-btn body-sb text-gray-60 h-fit w-full py-2"
29+
onClick={() => {
30+
navigate(`/`);
31+
sessionStorage.removeItem('onBoarding');
32+
}}
33+
>
34+
홈으로 가기
35+
</button>
36+
</main>
37+
);
38+
}

src/pages/Onboarding/components/Spinner.tsx

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,9 @@ const Spinner = ({ target, index }: SpinnerProps) => {
1313
const SPEED = 100 + 10 * index;
1414
const [position, setPosition] = useState(0);
1515
const [isRunning, setIsRunning] = useState(true);
16-
let LETTER_HEIGHT = 40;
16+
const LETTER_HEIGHT = 45;
1717
const animationFrameRef = useRef<number | null>(null);
1818

19-
//TODO: 여러 기기에서 실효성 확인
20-
// 웹에서는 없어도 될 것 같음
21-
// calculate full height of the cycle
22-
const containerRef = useRef<HTMLDivElement>(null);
23-
useEffect(() => {
24-
if (containerRef.current) {
25-
console.log(LETTER_HEIGHT);
26-
const letter = containerRef.current.querySelector('p');
27-
if (letter) {
28-
LETTER_HEIGHT = letter.getBoundingClientRect().height;
29-
}
30-
}
31-
console.log(LETTER_HEIGHT);
32-
}, []);
3319
const FULL_ROTATION = -TARGET_ARR.length * LETTER_HEIGHT;
3420

3521
useEffect(() => {
@@ -72,7 +58,6 @@ const Spinner = ({ target, index }: SpinnerProps) => {
7258
style={{ willChange: 'transform' }}
7359
>
7460
<div
75-
ref={containerRef}
7661
className="text-center transition-transform duration-500 ease-linear"
7762
style={{ transform: `translateY(${position}px)` }}
7863
>

src/pages/Onboarding/index.tsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,41 @@
1-
import { useState } from 'react';
1+
import { useEffect, useState } from 'react';
22

33
import SetZipCode from './SetZipCode';
44
import UserInteraction from './UserInteraction';
5+
import WelcomeLetter from './welcomeLetter';
56

67
const OnboardingPage = () => {
78
const [isZipCodeSet, setIsZipCodeSet] = useState<boolean>(false);
9+
const [isAnimationOver, setIsAnimationOver] = useState<boolean>(false);
810

11+
useEffect(() => {
12+
if (isZipCodeSet || isAnimationOver) {
13+
sessionStorage.setItem(
14+
'onBoarding',
15+
JSON.stringify({ isZipCodeSet: isZipCodeSet, isAnimationOver: isAnimationOver }),
16+
);
17+
}
18+
}, [isZipCodeSet, isAnimationOver]);
19+
20+
useEffect(() => {
21+
const prevDataString = sessionStorage.getItem('onBoarding');
22+
if (prevDataString) {
23+
const newData = JSON.parse(prevDataString);
24+
console.log(newData);
25+
setIsZipCodeSet(newData.isZipCodeSet);
26+
setIsAnimationOver(newData.isAnimationOver);
27+
console.log('isZipCode', isZipCodeSet, 'isAnimation', isAnimationOver);
28+
}
29+
}, []);
930
return (
1031
<main className="inset-0 mx-5 mt-20 mb-[1.875rem] flex grow flex-col items-center justify-between overflow-hidden">
11-
{isZipCodeSet ? <UserInteraction /> : <SetZipCode setIsZipCodeSet={setIsZipCodeSet} />}
32+
{!isZipCodeSet ? (
33+
<SetZipCode setIsZipCodeSet={setIsZipCodeSet} />
34+
) : !isAnimationOver ? (
35+
<UserInteraction setIsAnimationOver={setIsAnimationOver} />
36+
) : (
37+
<WelcomeLetter />
38+
)}
1239
</main>
1340
);
1441
};

0 commit comments

Comments
 (0)