Skip to content

Commit 8db29cb

Browse files
authored
Merge pull request #143 from prgrms-web-devcourse-final-project/142-feature/modal-animation
[Feature] #142 모달 애니메이션
2 parents 96ca6fe + 9d4fc25 commit 8db29cb

File tree

10 files changed

+164
-30
lines changed

10 files changed

+164
-30
lines changed

package-lock.json

Lines changed: 38 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"@tanstack/react-query": "^5.62.2",
2424
"@tanstack/react-query-devtools": "^5.62.2",
2525
"d3": "^7.9.0",
26+
"framer-motion": "^11.13.1",
2627
"ios-style-picker": "^0.0.6",
2728
"ol": "^10.2.1",
2829
"react": "^18.3.1",

src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ const MobileContainer = styled.div`
6363
left: 50%;
6464
top: 50%;
6565
translate: -50% -50%;
66-
66+
overflow: hidden;
6767
-ms-overflow-style: none;
6868
scrollbar-width: none;
6969

src/components/Profile/styles.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ type ProfileProps = {
99
export const Profile = styled.div<ProfileProps>`
1010
width: ${({ $size }) => $size + 'px'};
1111
height: ${({ $size }) => $size + 'px'};
12-
background: url(${({ $src }) => $src}) center/cover ${({ theme }) => theme.colors.brand.sub};
12+
background: url(${({ $src }) => $src}) center/cover ${({ theme }) => theme.colors.brand.lighten_2};
1313
cursor: ${({ $userId }) => ($userId ? 'pointer' : 'default')};
1414
border-radius: 50%;
1515
`

src/modals/ModalContainer/index.tsx

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,82 @@
1+
import { AnimatePresence, motion } from 'framer-motion'
12
import { useEffect } from 'react'
3+
import { AnimationType, useModalStore } from '~stores/modalStore'
24
import * as S from './styles'
3-
import { useModalStore } from '~stores/modalStore'
5+
6+
const animationVariants = {
7+
none: {
8+
initial: {},
9+
animate: {},
10+
exit: {},
11+
},
12+
fade: {
13+
initial: { opacity: 0, height: '100%' },
14+
animate: { opacity: 1, height: '100%' },
15+
exit: { opacity: 0, height: '100%' },
16+
},
17+
slideUp: {
18+
initial: { y: '100%', height: '100%' },
19+
animate: { y: 0, height: '100%' },
20+
exit: { y: '100%', height: '100%' },
21+
},
22+
slideDown: {
23+
initial: { y: -'100%', height: '100%' },
24+
animate: { y: 0, height: '100%' },
25+
exit: { y: -'100%', height: '100%' },
26+
},
27+
slideLeft: {
28+
initial: { x: '100%', height: '100%' },
29+
animate: { x: 0, height: '100%' },
30+
exit: { x: '100%', height: '100%' },
31+
},
32+
slideRight: {
33+
initial: { x: -'100%', height: '100%' },
34+
animate: { x: 0, height: '100%' },
35+
exit: { x: -'100%', height: '100%' },
36+
},
37+
}
38+
39+
const ModalAnimation = ({
40+
children,
41+
animationType,
42+
index,
43+
}: {
44+
children: React.ReactNode
45+
animationType?: AnimationType
46+
index: number
47+
}) => {
48+
const transitionConfig =
49+
animationType === 'fade'
50+
? {
51+
type: 'tween',
52+
duration: 0.15, // Shorter duration for fade animation
53+
delay: index * 0.05, // Adjusted delay for fade animation
54+
}
55+
: {
56+
type: 'tween',
57+
duration: 0.3,
58+
delay: index * 0.1,
59+
}
60+
return (
61+
<motion.div
62+
variants={animationVariants[animationType || 'fade']}
63+
initial='initial'
64+
animate='animate'
65+
exit='exit'
66+
transition={transitionConfig}
67+
style={{
68+
zIndex: 1000 + index, // 동적 z-index
69+
position: 'absolute',
70+
top: 0,
71+
left: 0,
72+
right: 0,
73+
bottom: 0,
74+
}}
75+
>
76+
{children}
77+
</motion.div>
78+
)
79+
}
480

581
export default function ModalContainer() {
682
const { modalList, popModal } = useModalStore()
@@ -19,5 +95,21 @@ export default function ModalContainer() {
1995
return () => window.removeEventListener('popstate', preventBack)
2096
}, [modalList])
2197

22-
return <>{modalList.length ? <S.ModalWrapper>{...modalList}</S.ModalWrapper> : null}</>
98+
return (
99+
<AnimatePresence>
100+
{modalList.length > 0 && (
101+
<S.ModalWrapper>
102+
{modalList.map((modal, index) => (
103+
<ModalAnimation
104+
key={modal.id || index} // 고유한 key 사용
105+
animationType={modal.animationType}
106+
index={index}
107+
>
108+
{modal.content}
109+
</ModalAnimation>
110+
))}
111+
</S.ModalWrapper>
112+
)}
113+
</AnimatePresence>
114+
)
23115
}

src/modals/ModalContainer/styles.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ export const ModalWrapper = styled.div`
66
left: 0;
77
width: 100%;
88
height: 100%;
9-
background-color: rgba(0, 0, 0, 0.4);
10-
z-index: 999;
9+
/* background-color: rgba(0, 0, 0, 0.4); */
10+
z-index: 900;
1111
`

src/modals/RegisterDogModal/DogProfileSection/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export default function DogProfileSection() {
3333
showToast(alertMessage)
3434
return
3535
}
36-
pushModal(<DogProfileDetailSection />)
36+
pushModal(<DogProfileDetailSection />, 'slideLeft')
3737
}
3838

3939
const handlePrevClick = () => {

src/pages/RegisterPage/Dog/SelectSectionButton/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export default function SelectSectionButton({ title, description, src, modal }:
1414
const { pushModal } = useModalStore()
1515

1616
return (
17-
<S.SelectSectionButton onClick={() => pushModal(modal)}>
17+
<S.SelectSectionButton onClick={() => pushModal(modal, 'slideLeft')}>
1818
<S.TypoWrapper>
1919
<Typo20 $weight='700'>{title}</Typo20>
2020
</S.TypoWrapper>

src/pages/RegisterPage/Register/index.tsx

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
1-
import * as S from './styles'
1+
import { useEffect } from 'react'
22
import { Helmet } from 'react-helmet-async'
3+
import { useNavigate, useSearchParams } from 'react-router-dom'
4+
import { createRegister } from '~apis/register/createRegister'
35
import AddOwnerAvatar from '~assets/add-dog-picture.svg'
6+
import { ActionButton } from '~components/Button/ActionButton'
47
import GenderSelectButton from '~components/GenderSelectButton'
58
import { Input } from '~components/Input'
9+
import Toast from '~components/Toast'
10+
import { FAMILY_ROLE } from '~constants/familyRole'
11+
import { useGeolocation } from '~hooks/useGeolocation'
12+
import FamilyRoleChoiceModal from '~modals/PositionChoiceModal'
613
import RegisterAvatarModal from '~modals/RegisterAvatarModal'
714
import { useModalStore } from '~stores/modalStore'
8-
import { ActionButton } from '~components/Button/ActionButton'
9-
import FamilyRoleChoiceModal from '~modals/PositionChoiceModal'
10-
import { useGeolocation } from '~hooks/useGeolocation'
1115
import { useOwnerProfileStore } from '~stores/ownerProfileStore'
12-
import { validateOwnerProfile } from '~utils/validateOwnerProfile'
13-
import RegisterDogPage from '~pages/RegisterPage/Dog'
14-
import Toast from '~components/Toast'
1516
import { useToastStore } from '~stores/toastStore'
16-
import { useSearchParams } from 'react-router-dom'
17-
import { createRegister } from '~apis/register/createRegister'
1817
import { FamilyRole } from '~types/common'
19-
import { useEffect } from 'react'
20-
import { FAMILY_ROLE } from '~constants/familyRole'
18+
import { validateOwnerProfile } from '~utils/validateOwnerProfile'
19+
import * as S from './styles'
2120

2221
export default function Register() {
2322
const { ownerProfile, setOwnerProfile } = useOwnerProfileStore()
@@ -27,7 +26,7 @@ export default function Register() {
2726
const [searchParams] = useSearchParams()
2827
const email = searchParams.get('email') || ''
2928
const provider = searchParams.get('provider') || ''
30-
29+
const navigate = useNavigate()
3130
const handleNextClick = async () => {
3231
const alertMessage = validateOwnerProfile(ownerProfile)
3332
if (alertMessage) {
@@ -55,7 +54,7 @@ export default function Register() {
5554
if (response.code === 201) {
5655
//? 채팅 구현을 위해 임의로 추가한 부분입니다.
5756
setOwnerProfile({ memberId: response.data.memberId })
58-
pushModal(<RegisterDogPage />)
57+
navigate('/register/dog')
5958
}
6059
} catch (error) {
6160
showToast(error instanceof Error ? error.message : '회원가입에 실패했습니다')

src/stores/modalStore.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,39 @@
11
import { ReactNode } from 'react'
22
import { create } from 'zustand'
33

4+
export type AnimationType = 'none' | 'fade' | 'slideUp' | 'slideDown' | 'slideLeft' | 'slideRight'
5+
6+
interface ModalItem {
7+
id: string
8+
content: ReactNode
9+
animationType?: AnimationType
10+
}
11+
412
interface ModalStore {
5-
modalList: ReactNode[]
6-
pushModal: (modal: ReactNode) => void
13+
modalList: ModalItem[]
14+
pushModal: (modal: ReactNode, animationType?: AnimationType) => void
715
popModal: () => void
816
clearModal: () => void
917
}
1018

1119
export const useModalStore = create<ModalStore>((set, get) => ({
1220
modalList: [],
13-
pushModal: modal => {
21+
pushModal: (content, animationType) => {
22+
const id = `modal-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
1423
set(state => ({
15-
modalList: [...state.modalList, modal],
24+
modalList: [...state.modalList, { id, content, animationType }],
1625
}))
17-
// 모달이 추가될 때 새로운 히스토리 항목 생성
1826
if (get().modalList.length > 0) {
1927
window.history.pushState({ modal: true }, '', window.location.href)
2028
}
2129
},
2230
popModal: () => {
23-
// 모달이 제거될 때 히스토리 뒤로가기
2431
set(state => ({
2532
modalList: state.modalList.slice(0, -1),
2633
}))
2734
},
2835
clearModal: () => {
2936
set({ modalList: [] })
30-
// 모든 모달 제거 시 히스토리 초기화
3137
const modalCount = get().modalList.length
3238
if (modalCount) window.history.go(-modalCount)
3339
},

0 commit comments

Comments
 (0)