diff --git a/frontend/src/components/common/Drawer/Drawer.styled.ts b/frontend/src/components/common/Drawer/Drawer.styled.ts
index de7021e90..93433e435 100644
--- a/frontend/src/components/common/Drawer/Drawer.styled.ts
+++ b/frontend/src/components/common/Drawer/Drawer.styled.ts
@@ -1,34 +1,41 @@
import styled from "@emotion/styled";
-export const DrawerContainer = styled.div<{ isOpen: boolean }>`
- display: flex;
- flex-direction: column;
+import theme from "@styles/theme";
+import { PRIMITIVE_COLORS } from "@styles/tokens";
+
+export const Overlay = styled.div<{ isOpen: boolean }>`
+ visibility: ${({ isOpen }) => (isOpen ? "visible" : "hidden")};
position: fixed;
- top: 0;
- right: ${({ isOpen }) => (isOpen ? "0" : "-210px")};
- z-index: ${({ theme }) => theme.zIndex.drawer};
- width: 210px;
+ z-index: ${({ theme }) => theme.zIndex.drawerOverlay};
+ width: 100%;
height: 100%;
+ inset: 0;
- background-color: #fff;
+ background-color: rgb(0 0 0 / 30%);
+ opacity: ${({ isOpen }) => (isOpen ? 1 : 0)};
- transition: right 0.3s ease-in-out;
+ animation: ${({ isOpen, theme }) => (isOpen ? theme.animation.fade.in : theme.animation.fade.out)}
+ ${theme.animation.duration.default} ease-in-out;
`;
-export const Overlay = styled.div<{ isOpen: boolean }>`
+export const DrawerContainer = styled.div<{ isOpen: boolean }>`
+ display: flex;
+ flex-direction: column;
visibility: ${({ isOpen }) => (isOpen ? "visible" : "hidden")};
position: fixed;
top: 0;
- left: 0;
- z-index: ${({ theme }) => theme.zIndex.drawerOverlay};
- width: 100%;
+ right: 0;
+ z-index: ${({ theme }) => theme.zIndex.drawer};
+ width: 210px;
height: 100%;
- background-color: rgb(0 0 0 / 30%);
- opacity: ${({ isOpen }) => (isOpen ? 1 : 0)};
- transition:
- opacity 0.3s ease-in-out,
- visibility 0.3s ease-in-out;
+ background-color: ${PRIMITIVE_COLORS.white};
+
+ animation: ${({ isOpen, theme }) =>
+ isOpen
+ ? theme.animation.slide.in({ from: "100%", to: 0 })
+ : theme.animation.slide.out({ from: 0, to: "100%" })}
+ ${theme.animation.duration.default} ease-in-out;
`;
export const DrawerHeader = styled.div`
@@ -46,9 +53,11 @@ export const DrawerContent = styled.div`
`;
export const TriggerButton = styled.button`
- background: none;
border: none;
+ background-color: none;
+
font-size: 1.5rem;
+
cursor: pointer;
`;
diff --git a/frontend/src/components/common/Drawer/Drawer.tsx b/frontend/src/components/common/Drawer/Drawer.tsx
index bc23750a1..c1958792e 100644
--- a/frontend/src/components/common/Drawer/Drawer.tsx
+++ b/frontend/src/components/common/Drawer/Drawer.tsx
@@ -6,16 +6,19 @@ import DrawerProvider, { useDrawerContext } from "@contexts/DrawerProvider";
import useModalControl from "@hooks/useModalControl";
import usePressESC from "@hooks/usePressESC";
import useToggle from "@hooks/useToggle";
+import useUnmountAnimation from "@hooks/useUnmountAnimation";
import VisuallyHidden from "../VisuallyHidden/VisuallyHidden";
import * as S from "./Drawer.styled";
const Drawer = ({ children }: React.PropsWithChildren) => {
const [isOpen, , , toggle] = useToggle();
- usePressESC(isOpen, toggle);
+ usePressESC(isOpen, toggle);
useModalControl(isOpen, toggle);
+ const { isRendered } = useUnmountAnimation({ isOpen });
+
let headerContent: React.ReactNode | null = null;
let drawerContent: React.ReactNode | null = null;
const otherContent: React.ReactNode[] = [];
@@ -38,13 +41,16 @@ const Drawer = ({ children }: React.PropsWithChildren) => {
{isOpen ? "사용자 메뉴가 열렸습니다." : "사용자 메뉴가 닫혔습니다."}
{otherContent}
-
- {isOpen &&
+
+ {isRendered &&
ReactDOM.createPortal(
-
- {headerContent}
- {drawerContent}
- ,
+ <>
+
+
+ {headerContent}
+ {drawerContent}
+
+ >,
document.body,
)}
diff --git a/frontend/src/components/common/FloatingButton/FloatingButton.styled.ts b/frontend/src/components/common/FloatingButton/FloatingButton.styled.ts
index a5dda5445..189315700 100644
--- a/frontend/src/components/common/FloatingButton/FloatingButton.styled.ts
+++ b/frontend/src/components/common/FloatingButton/FloatingButton.styled.ts
@@ -1,6 +1,7 @@
import { css } from "@emotion/react";
import styled from "@emotion/styled";
+import theme from "@styles/theme";
import { PRIMITIVE_COLORS } from "@styles/tokens";
export const FloatingButtonContainer = styled.div`
@@ -13,22 +14,27 @@ export const FloatingButtonContainer = styled.div`
z-index: ${({ theme }) => theme.zIndex.floating};
`;
-export const BackdropLayout = styled.div`
+export const BackdropLayout = styled.div<{ $isOpen: boolean }>`
+ visibility: ${({ $isOpen }) => ($isOpen ? "visible" : "hidden")};
position: fixed;
width: 100%;
height: 100%;
- inset: 0;
background-color: ${({ theme }) => theme.colors.dimmed};
+
+ animation: ${({ $isOpen, theme }) =>
+ $isOpen ? theme.animation.fade.in : theme.animation.fade.out}
+ ${theme.animation.duration.default} ease-in-out;
+ inset: 0;
cursor: pointer;
`;
export const SubButtonContainer = styled.div<{ $isOpen: boolean }>`
display: flex;
flex-direction: column;
+ visibility: ${({ $isOpen }) => ($isOpen ? "visible" : "hidden")};
position: absolute;
bottom: 100%;
- gap: ${({ theme }) => theme.spacing.l};
width: 16rem;
padding: ${({ theme }) => theme.spacing.l} ${({ theme }) => theme.spacing.m};
@@ -36,13 +42,13 @@ export const SubButtonContainer = styled.div<{ $isOpen: boolean }>`
background-color: ${PRIMITIVE_COLORS.gray[700]};
- transition: all 0.3s ease-out;
-
- ${({ $isOpen }) => css`
- opacity: ${$isOpen ? 1 : 0};
- visibility: ${$isOpen ? "visible" : "hidden"};
- transform: translateY(${$isOpen ? -0.8 : 2}rem);
- `}
+ animation: ${({ $isOpen, theme }) =>
+ $isOpen
+ ? theme.animation.slide.up({ from: 2, to: -0.8 })
+ : theme.animation.slide.down({ from: -0.8, to: 2 })}
+ ${theme.animation.duration.default} ease-in-out;
+ gap: ${({ theme }) => theme.spacing.l};
+ animation-fill-mode: forwards;
`;
export const SubButton = styled.button`
diff --git a/frontend/src/components/common/FloatingButton/FloatingButton.tsx b/frontend/src/components/common/FloatingButton/FloatingButton.tsx
index 7cf2a6eec..e50e2b174 100644
--- a/frontend/src/components/common/FloatingButton/FloatingButton.tsx
+++ b/frontend/src/components/common/FloatingButton/FloatingButton.tsx
@@ -2,6 +2,7 @@ import { useNavigate } from "react-router-dom";
import useModalControl from "@hooks/useModalControl";
import useToggle from "@hooks/useToggle";
+import useUnmountAnimation from "@hooks/useUnmountAnimation";
import { removeEmoji } from "@utils/removeEmojis";
@@ -23,6 +24,7 @@ const FloatingButton = () => {
};
useModalControl(isOpen, handleToggleButton);
+ const { isRendered } = useUnmountAnimation({ isOpen });
return (
@@ -31,9 +33,9 @@ const FloatingButton = () => {
? "여행기 및 여행 계획 작성 메뉴가 열렸습니다. 닫으려면 esc버튼을 눌러주세요."
: "여행기 및 여행 계획 작성 메뉴가 닫혔습니다."}
- {isOpen && (
+ {isRendered && (
<>
-
+
{SUB_BUTTONS.map(({ text, route }) => (
diff --git a/frontend/src/components/common/Icon/Icon.styled.ts b/frontend/src/components/common/Icon/Icon.styled.ts
new file mode 100644
index 000000000..d81b48623
--- /dev/null
+++ b/frontend/src/components/common/Icon/Icon.styled.ts
@@ -0,0 +1,7 @@
+import styled from "@emotion/styled";
+
+export const Wrapper = styled.div<{ size: string }>`
+ display: flex;
+ align-items: center;
+ height: ${({ size }) => size}px;
+`;
diff --git a/frontend/src/components/common/Icon/Icon.tsx b/frontend/src/components/common/Icon/Icon.tsx
index 20264f387..a6f1fef59 100644
--- a/frontend/src/components/common/Icon/Icon.tsx
+++ b/frontend/src/components/common/Icon/Icon.tsx
@@ -1,5 +1,6 @@
import { StrokeLineCap, StrokeLineJoin } from "@components/common/Icon/Icon.type";
+import * as S from "./Icon.styled";
import SVG_ICONS_MAP from "./svg-icons.json";
interface IconProps extends React.ComponentPropsWithoutRef<"svg"> {
@@ -10,24 +11,28 @@ interface IconProps extends React.ComponentPropsWithoutRef<"svg"> {
const Icon = ({ iconType, color, size, ...attributes }: IconProps) => {
return (
-
+
+
+
);
};
diff --git a/frontend/src/components/common/Modal/EditRegisterModalBottomSheet/EditRegisterModalBottomSheet.tsx b/frontend/src/components/common/Modal/EditRegisterModalBottomSheet/EditRegisterModalBottomSheet.tsx
index d298065af..3bd41aa7a 100644
--- a/frontend/src/components/common/Modal/EditRegisterModalBottomSheet/EditRegisterModalBottomSheet.tsx
+++ b/frontend/src/components/common/Modal/EditRegisterModalBottomSheet/EditRegisterModalBottomSheet.tsx
@@ -24,40 +24,38 @@ const EditRegisterModalBottomSheet = ({
onConfirm,
}: EditRegisterModalBottomSheetProps) => {
return (
- isOpen && (
-
-
-
-
-
-
-
- {mainText}
-
- {subText}
-
-
-
+
+
+
+
+
+
+
+ {mainText}
+
+ {subText}
+
+
+
-
-
-
-
-
- )
+
+
+
+
+
);
};
diff --git a/frontend/src/components/common/Modal/SingleSelectionTagModalBottomSheet/SingleSelectionTagModalBottomSheet.styled.ts b/frontend/src/components/common/Modal/SingleSelectionTagModalBottomSheet/SingleSelectionTagModalBottomSheet.styled.ts
index cb5768d52..e982403f6 100644
--- a/frontend/src/components/common/Modal/SingleSelectionTagModalBottomSheet/SingleSelectionTagModalBottomSheet.styled.ts
+++ b/frontend/src/components/common/Modal/SingleSelectionTagModalBottomSheet/SingleSelectionTagModalBottomSheet.styled.ts
@@ -16,4 +16,5 @@ export const HandleBar = styled.div`
export const modalBodyStyle = css`
align-items: flex-start;
gap: ${theme.spacing.l};
+ padding-bottom: ${theme.spacing.l};
`;
diff --git a/frontend/src/components/pages/main/MainPage.tsx b/frontend/src/components/pages/main/MainPage.tsx
index df648abca..5b3629cd6 100644
--- a/frontend/src/components/pages/main/MainPage.tsx
+++ b/frontend/src/components/pages/main/MainPage.tsx
@@ -19,6 +19,7 @@ import useKeyDown from "@hooks/useKeyDown/useKeyDown";
import useMultiSelectionTag from "@hooks/useMultiSelectionTag";
import useSingleSelectionTag from "@hooks/useSingleSelectionTag";
import useTravelogueCardFocus from "@hooks/useTravelogueCardFocus";
+import useUnmountAnimation from "@hooks/useUnmountAnimation";
import { ERROR_MESSAGE_MAP } from "@constants/errorMessage";
import { FORM_VALIDATIONS_MAP } from "@constants/formValidation";
@@ -114,6 +115,14 @@ const MainPage = () => {
const cardRefs = useTravelogueCardFocus(isFetchingNextPage);
+ const { isRendered: shouldSortingRender } = useUnmountAnimation({
+ isOpen: sorting.isModalOpen,
+ });
+
+ const { isRendered: shouldFilterRender } = useUnmountAnimation({
+ isOpen: travelPeriod.isModalOpen,
+ });
+
if (isPaused) {
alert(ERROR_MESSAGE_MAP.network);
}
@@ -271,7 +280,7 @@ const MainPage = () => {
? "여행기 정렬 메뉴가 열렸습니다."
: "여행기 정렬 메뉴가 닫혔습니다."}
- {sorting.isModalOpen && (
+ {shouldSortingRender && (
{
? "여행기 필터 메뉴가 열렸습니다."
: "여행기 필터 메뉴가 닫혔습니다."}
- {travelPeriod.isModalOpen && (
+ {shouldFilterRender && (
{
const { editModal, profileImage, profileNickname, profileEdit, userProfile } = useMyPage();
+ const { isRendered } = useUnmountAnimation({ isOpen: editModal.isEditModalOpen });
const showErrorAlert = (error: Error | null) => {
if (error && !IGNORED_ERROR_MESSAGES.includes(error.message)) alert(error.message);
@@ -126,7 +129,7 @@ const MyPage = () => {
css={S.listStyle}
/>
- {editModal.isEditModalOpen && (
+ {isRendered && (
{
const [isOpen, handleOpenBottomSheet, handleCloseBottomSheet] = useToggle();
+ const { isRendered } = useUnmountAnimation({ isOpen });
const {
state: {
@@ -197,14 +199,16 @@ const TravelogueEditPage = () => {
-
+ {isRendered && (
+
+ )}
>
);
};
diff --git a/frontend/src/constants/route.ts b/frontend/src/constants/route.ts
index f31a17cf3..ec9dbeabc 100644
--- a/frontend/src/constants/route.ts
+++ b/frontend/src/constants/route.ts
@@ -1,7 +1,7 @@
export const ROUTE_PATHS_MAP = {
back: -1,
root: "/",
- main: "main",
+ main: "/main",
travelogue: (id?: number | string) => (id ? `/travelogue/${id}` : "/travelogue/:id"),
travelPlan: (id?: number | string) => (id ? `/travel-plan/${id}` : "/travel-plan/:id"),
travelogueRegister: "/travelogue/register",
diff --git a/frontend/src/hooks/useUnmountAnimation.ts b/frontend/src/hooks/useUnmountAnimation.ts
new file mode 100644
index 000000000..3b6f0a479
--- /dev/null
+++ b/frontend/src/hooks/useUnmountAnimation.ts
@@ -0,0 +1,26 @@
+import { useEffect, useState } from "react";
+
+interface useUnmountAnimationProps {
+ isOpen: boolean;
+ animationDuration?: number;
+}
+
+const useUnmountAnimation = ({ isOpen, animationDuration = 300 }: useUnmountAnimationProps) => {
+ const [isRendered, setIsRendered] = useState(false);
+
+ useEffect(() => {
+ if (isOpen) {
+ setIsRendered(true);
+ } else {
+ const timer = setTimeout(() => {
+ setIsRendered(false);
+ }, animationDuration);
+
+ return () => clearTimeout(timer);
+ }
+ }, [isOpen, animationDuration]);
+
+ return { isRendered };
+};
+
+export default useUnmountAnimation;
diff --git a/frontend/src/styles/theme.ts b/frontend/src/styles/theme.ts
index 07b8e04df..4b053a916 100644
--- a/frontend/src/styles/theme.ts
+++ b/frontend/src/styles/theme.ts
@@ -1,12 +1,13 @@
import { Theme } from "@emotion/react";
-import { SEMANTIC_COLORS, SPACING, TYPOGRAPHY, Z_INDEX } from "@styles/tokens";
+import { ANIMATION, SEMANTIC_COLORS, SPACING, TYPOGRAPHY, Z_INDEX } from "@styles/tokens";
const theme: Theme = {
typography: TYPOGRAPHY,
colors: SEMANTIC_COLORS,
spacing: SPACING,
zIndex: Z_INDEX,
+ animation: ANIMATION,
};
export default theme;
diff --git a/frontend/src/styles/tokens/animation.ts b/frontend/src/styles/tokens/animation.ts
new file mode 100644
index 000000000..88714ffee
--- /dev/null
+++ b/frontend/src/styles/tokens/animation.ts
@@ -0,0 +1,82 @@
+import { keyframes } from "@emotion/react";
+
+interface SlideAnimationProps {
+ from: string | number;
+ to: string | number;
+}
+
+const formatValue = (value: string | number) => (typeof value === "number" ? `${value}rem` : value);
+
+export const ANIMATION = {
+ duration: {
+ default: "0.3s",
+ },
+ fade: {
+ in: keyframes`
+ from {
+ opacity: 0;
+ visibility: hidden;
+ }
+ to {
+ opacity: 1;
+ visibility: visible;
+ }
+ `,
+ out: keyframes`
+ from {
+ opacity: 1;
+ visibility: visible;
+ }
+ to {
+ opacity: 0;
+ visibility: hidden;
+ }
+ `,
+ },
+ slide: {
+ up: ({ from, to }: SlideAnimationProps) => keyframes`
+ from {
+ opacity: 0;
+ transform: translateY(${formatValue(from)});
+ visibility: hidden;
+ }
+ to {
+ opacity: 1;
+ transform: translateY(${formatValue(to)});
+ visibility: visible;
+ }
+ `,
+ down: ({ from, to }: SlideAnimationProps) => keyframes`
+ from {
+ opacity: 1;
+ transform: translateY(${formatValue(from)});
+ visibility: visible;
+ }
+ to {
+ opacity: 0;
+ transform: translateY(${formatValue(to)});
+ visibility: hidden;
+ }
+ `,
+ in: ({ from, to }: SlideAnimationProps) => keyframes`
+ from {
+ transform: translateX(${formatValue(from)});
+ visibility: visible;
+ }
+ to {
+ transform: translateX(${formatValue(to)});
+ visibility: hidden;
+ }
+ `,
+ out: ({ from, to }: SlideAnimationProps) => keyframes`
+ from {
+ transform: translateX(${formatValue(from)});
+ visibility: hidden;
+ }
+ to {
+ transform: translateX(${formatValue(to)});
+ visibility: visible;
+ }
+ `,
+ },
+};
diff --git a/frontend/src/styles/tokens/index.ts b/frontend/src/styles/tokens/index.ts
index 6819db706..9394ec341 100644
--- a/frontend/src/styles/tokens/index.ts
+++ b/frontend/src/styles/tokens/index.ts
@@ -2,3 +2,4 @@ export * from "./colors";
export * from "./typography";
export * from "./spacing";
export * from "./zIndex";
+export * from "./animation";
diff --git a/frontend/src/types/style/emotion.d.ts b/frontend/src/types/style/emotion.d.ts
index f88c79a91..2d2e50ccf 100644
--- a/frontend/src/types/style/emotion.d.ts
+++ b/frontend/src/types/style/emotion.d.ts
@@ -1,6 +1,6 @@
import "@emotion/react";
-import { SEMANTIC_COLORS, SPACING, TYPOGRAPHY, Z_INDEX } from "@styles/tokens";
+import { ANIMATION, SEMANTIC_COLORS, SPACING, TYPOGRAPHY, Z_INDEX } from "@styles/tokens";
declare module "@emotion/react" {
export interface Theme {
@@ -8,5 +8,6 @@ declare module "@emotion/react" {
colors: typeof SEMANTIC_COLORS;
spacing: typeof SPACING;
zIndex: typeof Z_INDEX;
+ animation: typeof ANIMATION;
}
}