Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"dev": "vite --host",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
Expand Down
Binary file added src/assets/images/envelope-pink-back-top.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/images/envelope-pink-front.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/images/envelope-pink.png
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지원님이 closed-letter.png라는 이름으로 에셋 추가해주셨는데 겹치는 것 같아요! 둘 중 하나로 통일해야할 것 같습니다

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제 이미지 삭제 후, 지원님 이미지로 통일했습니다!
envelope-pink-front -> opened-letter-front
envelope-pink -> closed-letter

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions src/pages/Onboarding/SetZipCode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';
import Spinner from './components/Spinner';

const SetZipCode = ({
setIsZipCodeSet,
}: {
setIsZipCodeSet: React.Dispatch<React.SetStateAction<Boolean>>;
}) => {
const DUMMY_ZIPCODE = '122A2';

return (
<>
<header className="flex flex-col items-center">
<h1 className="message-header body-b mb-2">우편번호를 설정하고 있습니다.</h1>
<p className="caption-sb text-gray-60">우편번호란?</p>
<p className="caption-sb text-gray-60">사용자님이 편지를 주고 받는 주소입니다.</p>
</header>
<section className="flex gap-2">
{DUMMY_ZIPCODE.split('').map((char, index) => (
<Spinner key={index} target={`${char}`} index={index}></Spinner>
))}
</section>
<button
type="button"
className="primary-btn body-m w-full py-2"
onClick={() => setIsZipCodeSet(true)}
>
다음으로
</button>
</>
);
};

export default SetZipCode;
146 changes: 146 additions & 0 deletions src/pages/Onboarding/UserInteraction.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

뭔가 엄청난 싸움을 하신 것 같은데요...!!!! 수고하셨습니다

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

휘유~

Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import envelope from '@/assets/images/envelope-pink.png';
import envelopeFront from '@/assets/images/envelope-pink-front.png';
import envelopeTop from '@/assets/images/envelope-pink-back-top.png';

import { useState, useRef, useEffect } from 'react';
import { twMerge } from 'tailwind-merge';
import { Link } from 'react-router';

export default function UserInteraction() {
const imgRef = useRef<HTMLImageElement>(null);
const [imgPos, setImgPos] = useState<{ top: number; width: number }>({ top: 0, width: 0 });
const [imgToBottom, setImgToBottom] = useState<Boolean>(false);

const [startAnimation, setStartAnimation] = useState<Boolean>(false);
const [openAnimation, setOpenAnimation] = useState<Boolean>(false);
const [letterOutAnimation, setLetterOutAnimation] = useState<Boolean>(false);
const [envelopeOut, setEnvelopeOut] = useState<Boolean>(false);
const [finishAnimation, setFinishAnimation] = useState<Boolean>(false);

const handleLetterClick = () => {
if (imgRef.current) {
const rect = imgRef.current.getBoundingClientRect();
setImgPos({ top: rect.top, width: rect.width });
}
setStartAnimation(true);
setTimeout(() => {
setImgToBottom(true);
}, 1000);
};

useEffect(() => {
if (imgToBottom) {
setTimeout(() => {
setOpenAnimation(true);
}, 1000);
}
}, [imgToBottom]);

useEffect(() => {
if (openAnimation) {
setTimeout(() => {
setLetterOutAnimation(true);
}, 2000);
}
}, [openAnimation]);

useEffect(() => {
if (letterOutAnimation) {
setTimeout(() => {
setEnvelopeOut(true);
}, 2000);
}
}, [letterOutAnimation]);

useEffect(() => {
if (envelopeOut) {
setTimeout(() => {
setFinishAnimation(true);
}, 2000);
}
}, [envelopeOut]);
if (startAnimation === false) {
return (
<>
<header className="flex flex-col items-center">
<h1 className="message-header body-b mb-2">이제 편지를 보내러 가볼까요?</h1>
</header>
<section className="mt-25 flex w-full grow flex-col place-items-center items-center px-10">
<p className="comment caption-m animate-float mb-8">편지를 눌러보세요!</p>
<img
role="button"
ref={imgRef}
src={envelope}
alt="분홍색 편지지"
className="h-auto w-full rounded transition-transform duration-1000 ease-in-out hover:scale-105"
onClick={handleLetterClick}
/>
</section>
</>
);
} else {
return (
<>
<img
src={envelopeFront}
alt="분홍색 편지지"
className={twMerge(
`z-30 mx-10 h-auto rounded transition-transform duration-1000 ease-in-out`,
imgToBottom && 'translate-y-full',
envelopeOut && 'animate-envelopeOut',
)}
style={{
top: `${imgPos.top}px`,
position: 'absolute',
width: `${imgPos.width}px`,
}}
/>
{letterOutAnimation && (
<div
className="animate-expandScale to-gray-5 z-20 max-w-[600px] rounded bg-linear-to-b from-white"
style={{
width: `${imgPos.width - imgPos.width * 0.1}px`,
bottom: `${imgPos.top - 0.7 * imgPos.top}px`,
top: `${imgPos.top - 0.5 * imgPos.top}px`,
position: 'absolute',
}}
></div>
)}
{openAnimation && (
<img
src={envelopeTop}
alt=""
className={twMerge(
`z-10 mx-10 h-auto rounded transition-transform duration-1000 ease-in-out`,
imgToBottom && 'translate-y-full',
openAnimation && 'animate-openEnvelope',
envelopeOut && 'animate-envelopeOut',
)}
style={{
top: `${imgPos.top}px`,
position: 'absolute',
width: `${imgPos.width}px`,
transformOrigin: 'bottom',
}}
/>
)}
<img
src={envelope}
alt="분홍색 편지지"
className={twMerge(
`z-0 mx-10 h-auto rounded transition-transform duration-1000 ease-in-out`,
imgToBottom && 'translate-y-full',
envelopeOut && 'animate-envelopeOut',
)}
style={{
top: `${imgPos.top}px`,
position: 'absolute',
width: `${imgPos.width}px`,
}}
/>
{/* TODO: 편지지 링크 */}
{finishAnimation && <Link to={'/'}></Link>}
</>
);
}
}
85 changes: 85 additions & 0 deletions src/pages/Onboarding/components/Spinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { useEffect, useRef, useState } from 'react';
import { ELEMENTS } from '../constants/index';

interface SpinnerProps {
target: string;
index: number;
}

const Spinner = ({ target, index }: SpinnerProps) => {
const newArr = ELEMENTS.filter((item) => item !== target);
const TARGET_ARR = [target, ...newArr.sort(() => Math.random() - 0.5), target];
const SPEED = 100 + 10 * index;
const [position, setPosition] = useState(0);
const [isRunning, setIsRunning] = useState(true);
let LETTER_HEIGHT = 40;
const animationFrameRef = useRef<number | null>(null);

// calculate full height of the cycle
const containerRef = useRef<HTMLDivElement>(null);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

containerRef 선언은 되어 있는데 연결되어 있지 않은 것 같아요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오호.. 일단 연결해 두었습니다! 웹에서는 차이가 크게 없어서 빼도 될 것 같은데, 모바일에서 어떨지 모르겠어서 일단 뺼지 둘지는 TODO로 남겨 두었습니당

useEffect(() => {
if (containerRef.current) {
const letter = containerRef.current.querySelector('p');
if (letter) {
LETTER_HEIGHT = letter.getBoundingClientRect().height;
}
}
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

의존성 배열이 없어서 렌더링될 때마다 다시 실행할 것 같은데 괜찮나용???

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

처음 렌더링 될때만 실행되도록 의존성 배열 추가했습니다!

const FULL_ROTATION = -TARGET_ARR.length * LETTER_HEIGHT;

useEffect(() => {
if (!isRunning) return;

let lastTime = performance.now();
const frameRate = 1000 / 60;

const animate = (time: number) => {
const deltaTime = time - lastTime;
if (deltaTime >= frameRate) {
setPosition((prev) => {
let newPos = prev - LETTER_HEIGHT * (deltaTime / SPEED);

if (newPos < FULL_ROTATION) {
newPos = 0;
setIsRunning(false);
return newPos;
}
return newPos;
});
lastTime = time;
}

animationFrameRef.current = requestAnimationFrame(animate);
};

animationFrameRef.current = requestAnimationFrame(animate);
Comment on lines +56 to +59
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 문제 없는 게 맞을까요? animate 안에 animate를 호출하고 있어서요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵넵! 보통 requestAnimationFrame은 이런식으로 사용한다고 합니다! requestAnimationFrame에 등록된 콜백함수들을 비동기로 호출하기 때문에 상관 없어요!

https://inpa.tistory.com/entry/%F0%9F%8C%90-requestAnimationFrame-%EA%B0%80%EC%9D%B4%EB%93%9C

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하!! 블로그 내용보고 살짝 겁먹었지만 시간날 때 천천히 읽어볼게요!!!


return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [isRunning]);

return (
<div
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"
style={{ willChange: 'transform' }}
>
<div
className="text-center transition-transform duration-100 ease-linear"
style={{ transform: `translateY(${position}px)`, transitionDuration: '500ms' }}
>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인라인 스타일 duration이랑 tailwind duration이 충돌나고 있는 것 같습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 확인 감사합니당

{TARGET_ARR.map((item, index) => {
return (
<p key={index} className="h1-b">
{item}
</p>
);
})}
</div>
</div>
);
};

export default Spinner;
38 changes: 38 additions & 0 deletions src/pages/Onboarding/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export const ELEMENTS = [
1,
2,
3,
4,
5,
6,
7,
8,
9,
0,
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
];
16 changes: 15 additions & 1 deletion src/pages/Onboarding/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
import { useState } from 'react';
import SetZipCode from './SetZipCode';
import UserInteraction from './UserInteraction';

const OnboardingPage = () => {
return <div>OnboardingPage</div>;
const [isZipCodeSet, setIsZipCodeSet] = useState<Boolean>(false);

return (
<main className="inset-0 mx-5 mt-20 mb-[1.875rem] flex grow flex-col items-center justify-between overflow-hidden">
{isZipCodeSet === false ? (
<SetZipCode setIsZipCodeSet={setIsZipCodeSet} />
) : (
<UserInteraction />
)}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

굉장히 사소한거지만 이렇게 바꿔도 좋을 것 같아요!

Suggested change
{isZipCodeSet === false ? (
<SetZipCode setIsZipCodeSet={setIsZipCodeSet} />
) : (
<UserInteraction />
)}
{isZipCodeSet ? (
<UserInteraction />
) : (
<SetZipCode setIsZipCodeSet={setIsZipCodeSet} />
)}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정했습니당

</main>
);
};

export default OnboardingPage;
5 changes: 5 additions & 0 deletions src/styles/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,9 @@
linear-gradient(180deg, #ffedae 0%, #eec297 100%);
background-blend-mode: overlay, normal;
}

.message-header {
@apply text-gray-60 w-fit rounded-full bg-white px-6 py-4;
}

}
Loading