Skip to content

Commit 59d89d4

Browse files
nirii00AAminhawldnjs990
authored
design: 온보딩 페이지 퍼블리싱 (#18)
* design: 온보딩 페이지 퍼블리싱 우편번호 페이지 퍼블리싱 완료 우편번호 애니메이션 구현 완료 애니메이션 페이지 퍼블리싱 완료 애니메이션 페이지 구현 중 관리자 편지는 컴포넌트 제작 후 라우팅 연결할 예정 * design: 게시판 퍼블리싱 (#14) * design: 편지 게시판 페이지 내 편지 미리보기 컴포넌트 퍼블리싱 * design: 롤링페이퍼 공지 컴포넌트 퍼블리싱 * design: 편지 공유 게시판 퍼블리싱 * refactor: ConfirmModal 배경 이미지 삽입 방식 개선 * rename: 파일명 형식 통일을 위한 에셋 파일명 수정 * design: 롤링페이퍼 편지 컴포넌트 퍼블리싱 * design: 롤링페이퍼 페이지 퍼블리싱 * refactor: ModalOverlay 컴포넌트로 모달의 외부 영역 클릭 로직 이동 * design: 롤링페이퍼 페이지 메시지 상세조회 모달 퍼블리싱 * design: 모달 활성화 시, 스크롤이 안되도록 수정 * design: 신고 모달 컴포넌트 퍼블리싱 * design: 롤링페이퍼 메사지 추가 모달 버픕ㄹ리싱 * design: Header 컴포넌트 position을 fixed로 변경 * refactor: 하단 배경 컴포넌트로 분리 * design: 마이페이지 중 내가 올린 게시물 페이지 퍼블리싱 * design: 게시판 상세 페이지의 편지 컴포넌트 퍼블리싱 * design: 편지 공유 게시글 상세 페이지 퍼블리싱 * feat: 게시글 상세 페이지 헤더 컴포넌트 뒤로가기 기능 추가 * remove: 중복 에셋 제거 * refactor: 중복 스타일 코드 utilities로 분리 * design: 내 편지함 페이지 퍼블리싱 (#16) * design: 내 편지함 목록 페이지 퍼블리싱 * refactor: 리스트 디자인 컴포넌트로 분리 * design: 컴포넌트 props명 수정 및 sender, receiver 디자인 변경 * design: 내 편지함 상세 페이지 퍼블리싱 * refactor: 배경 이미지 삽입되는 로직 컴포넌트로 분리 * design: 필요한 모달 퍼블리싱 및 버튼과 연결 * rename: ListItemContainer -> ListItemWrapper로 컴포넌트명 수정 * refactor: 페이지 제목에 PageTitle 컴포넌트 적용 * design: 온보딩 페이지 편지 애니메이션 구현 - 온보딩 페이지에서 편지 애니메이션 구현 - TODO: 모바일 뷰 확인 - TODO: 편지 상세 링크 * design : 랜덤 편지 페이지 & 편지 상세 페이지 퍼블리싱 (#17) * design:랜덤편지 메인화면, 편지선택 모달 퍼블리싱 * chore:스와이퍼 설치 * design:랜덤 편지 페이지 스와이퍼 적용 * design:편지 상세보기 퍼블리싱 * design:편지 작성 페이지 레이아웃 관련 수정 * design:랜덤편지 페이지 퍼블리싱 * design:편지 상세 페이지 퍼블리싱 * design:랜덤편지 세부사항 수정 & 공용 컴포넌트들로 코드 수정 * refactor:코드리뷰 수정사항 반영 * refactor: pr 피드백 반영 - 이미지 에셋 정리 - animation 파일 분리 - css 충돌 해결 - spinner -> containerRef 오류 수정 --------- Co-authored-by: nirii00 <[email protected]> Co-authored-by: Minha Ahn <[email protected]> Co-authored-by: wldnjs990 <[email protected]>
1 parent a8e7622 commit 59d89d4

File tree

12 files changed

+406
-2
lines changed

12 files changed

+406
-2
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"version": "0.0.0",
55
"type": "module",
66
"scripts": {
7-
"dev": "vite",
7+
"dev": "vite --host",
88
"build": "tsc -b && vite build",
99
"lint": "eslint .",
1010
"preview": "vite preview"
43.7 KB
Loading
19.5 KB
Loading
127 KB
Loading
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from 'react';
2+
import Spinner from './components/Spinner';
3+
4+
const SetZipCode = ({
5+
setIsZipCodeSet,
6+
}: {
7+
setIsZipCodeSet: React.Dispatch<React.SetStateAction<Boolean>>;
8+
}) => {
9+
const DUMMY_ZIPCODE = '122A2';
10+
11+
return (
12+
<>
13+
<header className="flex flex-col items-center">
14+
<h1 className="message-header body-b mb-2">우편번호를 설정하고 있습니다.</h1>
15+
<p className="caption-sb text-gray-60">우편번호란?</p>
16+
<p className="caption-sb text-gray-60">사용자님이 편지를 주고 받는 주소입니다.</p>
17+
</header>
18+
<section className="flex gap-2">
19+
{DUMMY_ZIPCODE.split('').map((char, index) => (
20+
<Spinner key={index} target={`${char}`} index={index}></Spinner>
21+
))}
22+
</section>
23+
<button
24+
type="button"
25+
className="primary-btn body-m w-full py-2"
26+
onClick={() => setIsZipCodeSet(true)}
27+
>
28+
다음으로
29+
</button>
30+
</>
31+
);
32+
};
33+
34+
export default SetZipCode;
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import envelope from '@/assets/images/closed-letter.png';
2+
import envelopeFront from '@/assets/images/opened-letter-front.png';
3+
import envelopeTop from '@/assets/images/envelope-pink-back-top.png';
4+
5+
import { useState, useRef, useEffect } from 'react';
6+
import { twMerge } from 'tailwind-merge';
7+
import { Link } from 'react-router';
8+
9+
export default function UserInteraction() {
10+
const imgRef = useRef<HTMLImageElement>(null);
11+
const [imgPos, setImgPos] = useState<{ top: number; width: number }>({ top: 0, width: 0 });
12+
const [imgToBottom, setImgToBottom] = useState<Boolean>(false);
13+
14+
const [startAnimation, setStartAnimation] = useState<Boolean>(false);
15+
const [openAnimation, setOpenAnimation] = useState<Boolean>(false);
16+
const [letterOutAnimation, setLetterOutAnimation] = useState<Boolean>(false);
17+
const [envelopeOut, setEnvelopeOut] = useState<Boolean>(false);
18+
const [finishAnimation, setFinishAnimation] = useState<Boolean>(false);
19+
20+
const handleLetterClick = () => {
21+
if (imgRef.current) {
22+
const rect = imgRef.current.getBoundingClientRect();
23+
setImgPos({ top: rect.top, width: rect.width });
24+
}
25+
setStartAnimation(true);
26+
setTimeout(() => {
27+
setImgToBottom(true);
28+
}, 1000);
29+
};
30+
31+
useEffect(() => {
32+
if (imgToBottom) {
33+
setTimeout(() => {
34+
setOpenAnimation(true);
35+
}, 1000);
36+
}
37+
}, [imgToBottom]);
38+
39+
useEffect(() => {
40+
if (openAnimation) {
41+
setTimeout(() => {
42+
setLetterOutAnimation(true);
43+
}, 2000);
44+
}
45+
}, [openAnimation]);
46+
47+
useEffect(() => {
48+
if (letterOutAnimation) {
49+
setTimeout(() => {
50+
setEnvelopeOut(true);
51+
}, 2000);
52+
}
53+
}, [letterOutAnimation]);
54+
55+
useEffect(() => {
56+
if (envelopeOut) {
57+
setTimeout(() => {
58+
setFinishAnimation(true);
59+
}, 2000);
60+
}
61+
}, [envelopeOut]);
62+
if (startAnimation === false) {
63+
return (
64+
<>
65+
<header className="flex flex-col items-center">
66+
<h1 className="message-header body-b mb-2">이제 편지를 보내러 가볼까요?</h1>
67+
</header>
68+
<section className="mt-25 flex w-full grow flex-col place-items-center items-center px-10">
69+
<p className="comment caption-m animate-float mb-8">편지를 눌러보세요!</p>
70+
<img
71+
role="button"
72+
ref={imgRef}
73+
src={envelope}
74+
alt="분홍색 편지지"
75+
className="h-auto w-full rounded transition-transform duration-1000 ease-in-out hover:scale-105"
76+
onClick={handleLetterClick}
77+
/>
78+
</section>
79+
</>
80+
);
81+
} else {
82+
return (
83+
<>
84+
<img
85+
src={envelopeFront}
86+
alt="분홍색 편지지"
87+
className={twMerge(
88+
`z-30 mx-10 h-auto rounded transition-transform duration-1000 ease-in-out`,
89+
imgToBottom && 'translate-y-full',
90+
envelopeOut && 'animate-envelopeOut',
91+
)}
92+
style={{
93+
top: `${imgPos.top}px`,
94+
position: 'absolute',
95+
width: `${imgPos.width}px`,
96+
}}
97+
/>
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 && (
110+
<img
111+
src={envelopeTop}
112+
alt=""
113+
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',
117+
envelopeOut && 'animate-envelopeOut',
118+
)}
119+
style={{
120+
top: `${imgPos.top}px`,
121+
position: 'absolute',
122+
width: `${imgPos.width}px`,
123+
transformOrigin: 'bottom',
124+
}}
125+
/>
126+
)}
127+
<img
128+
src={envelope}
129+
alt="분홍색 편지지"
130+
className={twMerge(
131+
`z-0 mx-10 h-auto rounded transition-transform duration-1000 ease-in-out`,
132+
imgToBottom && 'translate-y-full',
133+
envelopeOut && 'animate-envelopeOut',
134+
)}
135+
style={{
136+
top: `${imgPos.top}px`,
137+
position: 'absolute',
138+
width: `${imgPos.width}px`,
139+
}}
140+
/>
141+
{/* TODO: 편지지 링크 */}
142+
{finishAnimation && <Link to={'/'}></Link>}
143+
</>
144+
);
145+
}
146+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { useEffect, useRef, useState } from 'react';
2+
import { ELEMENTS } from '../constants/index';
3+
4+
interface SpinnerProps {
5+
target: string;
6+
index: number;
7+
}
8+
9+
const Spinner = ({ target, index }: SpinnerProps) => {
10+
const newArr = ELEMENTS.filter((item) => item !== target);
11+
const TARGET_ARR = [target, ...newArr.sort(() => Math.random() - 0.5), target];
12+
const SPEED = 100 + 10 * index;
13+
const [position, setPosition] = useState(0);
14+
const [isRunning, setIsRunning] = useState(true);
15+
let LETTER_HEIGHT = 40;
16+
const animationFrameRef = useRef<number | null>(null);
17+
18+
//TODO: 여러 기기에서 실효성 확인
19+
// 웹에서는 없어도 될 것 같음
20+
// calculate full height of the cycle
21+
const containerRef = useRef<HTMLDivElement>(null);
22+
useEffect(() => {
23+
if (containerRef.current) {
24+
console.log(LETTER_HEIGHT);
25+
const letter = containerRef.current.querySelector('p');
26+
if (letter) {
27+
LETTER_HEIGHT = letter.getBoundingClientRect().height;
28+
}
29+
}
30+
console.log(LETTER_HEIGHT);
31+
}, []);
32+
const FULL_ROTATION = -TARGET_ARR.length * LETTER_HEIGHT;
33+
34+
useEffect(() => {
35+
if (!isRunning) return;
36+
37+
let lastTime = performance.now();
38+
const frameRate = 1000 / 60;
39+
40+
const animate = (time: number) => {
41+
const deltaTime = time - lastTime;
42+
if (deltaTime >= frameRate) {
43+
setPosition((prev) => {
44+
let newPos = prev - LETTER_HEIGHT * (deltaTime / SPEED);
45+
46+
if (newPos < FULL_ROTATION) {
47+
newPos = 0;
48+
setIsRunning(false);
49+
return newPos;
50+
}
51+
return newPos;
52+
});
53+
lastTime = time;
54+
}
55+
56+
animationFrameRef.current = requestAnimationFrame(animate);
57+
};
58+
59+
animationFrameRef.current = requestAnimationFrame(animate);
60+
61+
return () => {
62+
if (animationFrameRef.current) {
63+
cancelAnimationFrame(animationFrameRef.current);
64+
}
65+
};
66+
}, [isRunning]);
67+
68+
return (
69+
<div
70+
className="bg-gray-10 flex h-13.5 w-10 -translate-y-20 flex-col items-center overflow-hidden rounded-sm inset-shadow-[0_4px_4px_0] inset-shadow-black/10"
71+
style={{ willChange: 'transform' }}
72+
>
73+
<div
74+
ref={containerRef}
75+
className="text-center transition-transform duration-500 ease-linear"
76+
style={{ transform: `translateY(${position}px)` }}
77+
>
78+
{TARGET_ARR.map((item, index) => {
79+
return (
80+
<p key={index} className="h1-b">
81+
{item}
82+
</p>
83+
);
84+
})}
85+
</div>
86+
</div>
87+
);
88+
};
89+
90+
export default Spinner;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
export const ELEMENTS = [
2+
1,
3+
2,
4+
3,
5+
4,
6+
5,
7+
6,
8+
7,
9+
8,
10+
9,
11+
0,
12+
'A',
13+
'B',
14+
'C',
15+
'D',
16+
'E',
17+
'F',
18+
'G',
19+
'H',
20+
'I',
21+
'J',
22+
'K',
23+
'L',
24+
'M',
25+
'N',
26+
'O',
27+
'P',
28+
'Q',
29+
'R',
30+
'S',
31+
'T',
32+
'U',
33+
'V',
34+
'W',
35+
'X',
36+
'Y',
37+
'Z',
38+
];

src/pages/Onboarding/index.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
1+
import { useState } from 'react';
2+
import SetZipCode from './SetZipCode';
3+
import UserInteraction from './UserInteraction';
4+
15
const OnboardingPage = () => {
2-
return <div>OnboardingPage</div>;
6+
const [isZipCodeSet, setIsZipCodeSet] = useState<Boolean>(false);
7+
8+
return (
9+
<main className="inset-0 mx-5 mt-20 mb-[1.875rem] flex grow flex-col items-center justify-between overflow-hidden">
10+
{isZipCodeSet ? <UserInteraction /> : <SetZipCode setIsZipCodeSet={setIsZipCodeSet} />}
11+
</main>
12+
);
313
};
414

515
export default OnboardingPage;

src/styles/animations.css

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,53 @@
5353
transform: perspective(800px) rotateY(0deg);
5454
}
5555
}
56+
57+
/* onBoarding */
58+
/* comment floating */
59+
@keyframes float {
60+
0%,
61+
100% {
62+
transform: translateY(0);
63+
}
64+
50% {
65+
transform: translateY(5px);
66+
}
67+
}
68+
69+
/* onBoarding */
70+
/* letter opening */
71+
@keyframes openEnvelope {
72+
0% {
73+
transform: rotateX(90deg);
74+
transform-origin: bottom;
75+
}
76+
100% {
77+
transform: rotateX(0deg);
78+
transform-origin: bottom;
79+
}
80+
}
81+
82+
/* onBoarding */
83+
/* letter coming out */
84+
@keyframes expandScale {
85+
0% {
86+
transform: scaleY(0);
87+
transform-origin: bottom;
88+
}
89+
100% {
90+
transform: scaleY(1);
91+
transform-origin: bottom;
92+
}
93+
}
94+
95+
/* onBoarding */
96+
/* envelope out animation*/
97+
@keyframes envelopeOut {
98+
0% {
99+
transform: translateY(0);
100+
}
101+
100% {
102+
transform: translateY(100vh);
103+
}
104+
}
56105
}

0 commit comments

Comments
 (0)