diff --git a/client/package.json b/client/package.json index 05cdafa6..2f79c064 100644 --- a/client/package.json +++ b/client/package.json @@ -16,7 +16,8 @@ "framer-motion": "^11.11.11", "react": "^18.3.1", "react-dom": "^18.3.1", - "socket.io-client": "^4.8.1" + "socket.io-client": "^4.8.1", + "zustand": "^5.0.1" }, "devDependencies": { "@pandabox/prettier-plugin": "^0.1.3", diff --git a/client/src/components/sidebar/Sidebar.animation.ts b/client/src/components/sidebar/Sidebar.animation.ts index 72b3333a..cef9e553 100644 --- a/client/src/components/sidebar/Sidebar.animation.ts +++ b/client/src/components/sidebar/Sidebar.animation.ts @@ -1,3 +1,5 @@ +import { SIDE_BAR } from "@constants/size"; + export const animation = { initial: { x: -100, @@ -14,3 +16,30 @@ export const animation = { duration: 0.5, }, }; + +export const sidebarVariants = { + open: { + width: `${SIDE_BAR.WIDTH}px`, + transition: { duration: 0.2, ease: "easeOut" }, + }, + closed: { + width: `${SIDE_BAR.MIN_WIDTH}px`, + }, +}; + +export const contentVariants = { + open: { + opacity: 1, + x: 0, + display: "flex", + transition: { delay: 0.2 }, + }, + closed: { + opacity: 0, + x: -20, + transitionEnd: { + delay: 0.2, + display: "none", + }, + }, +}; diff --git a/client/src/components/sidebar/Sidebar.style.ts b/client/src/components/sidebar/Sidebar.style.ts index 8ab02515..6f5c3fad 100644 --- a/client/src/components/sidebar/Sidebar.style.ts +++ b/client/src/components/sidebar/Sidebar.style.ts @@ -7,7 +7,6 @@ export const sidebarContainer = cx( display: "flex", gap: "lg", flexDirection: "column", - width: "sidebar.width", height: "calc(100vh - 40px)", marginBlock: "20px", }), @@ -24,3 +23,13 @@ export const plusIconBox = css({ justifyContent: "start", paddingInline: "md", }); + +export const sidebarToggleButton = css({ + zIndex: 10, + position: "absolute", + top: "4px", + right: "16px", + color: "gray.500", + fontSize: "24px", + cursor: "pointer", +}); diff --git a/client/src/components/sidebar/Sidebar.tsx b/client/src/components/sidebar/Sidebar.tsx index a1dd4f47..53849c19 100644 --- a/client/src/components/sidebar/Sidebar.tsx +++ b/client/src/components/sidebar/Sidebar.tsx @@ -1,10 +1,11 @@ -import { motion, AnimatePresence } from "framer-motion"; +import { motion } from "framer-motion"; import { IconButton } from "@components/button/IconButton"; +import { useIsSidebarOpen, useSidebarActions } from "src/stores/useSidebarStore"; import { Page } from "src/types/page"; import { MenuButton } from "./MenuButton"; import { PageItem } from "./PageItem"; -import { animation } from "./Sidebar.animation"; -import { sidebarContainer, navWrapper, plusIconBox } from "./Sidebar.style"; +import { animation, contentVariants, sidebarVariants } from "./Sidebar.animation"; +import { sidebarContainer, navWrapper, plusIconBox, sidebarToggleButton } from "./Sidebar.style"; export const Sidebar = ({ pages, @@ -15,26 +16,36 @@ export const Sidebar = ({ handlePageAdd: () => void; handlePageSelect: (pageId: number, isSidebar: boolean) => void; }) => { + const isSidebarOpen = useIsSidebarOpen(); + const { toggleSidebar } = useSidebarActions(); + return ( - + +
+ {isSidebarOpen ? "«" : "»"} +
+ + + + {pages?.map((item) => ( + + handlePageSelect(item.id, true)} /> + + ))} +
+ +
+
+
); }; diff --git a/client/src/constants/size.ts b/client/src/constants/size.ts index 88385233..9cdfb739 100644 --- a/client/src/constants/size.ts +++ b/client/src/constants/size.ts @@ -7,5 +7,6 @@ export const PAGE = { }; export const SIDE_BAR = { + MIN_WIDTH: 40, WIDTH: 300, }; diff --git a/client/src/features/page/hooks/usePage.ts b/client/src/features/page/hooks/usePage.ts index ee5c3690..30defcd6 100644 --- a/client/src/features/page/hooks/usePage.ts +++ b/client/src/features/page/hooks/usePage.ts @@ -1,6 +1,7 @@ +import { useEffect, useState } from "react"; import { PAGE, SIDE_BAR } from "@constants/size"; import { SPACING } from "@constants/spacing"; -import { useState } from "react"; +import { useIsSidebarOpen } from "src/stores/useSidebarStore"; import { Position, Size } from "src/types/page"; const PADDING = SPACING.MEDIUM * 2; @@ -12,6 +13,35 @@ export const usePage = ({ x, y }: Position) => { height: PAGE.HEIGHT, }); + const isSidebarOpen = useIsSidebarOpen(); + + const getSidebarWidth = () => (isSidebarOpen ? SIDE_BAR.WIDTH : SIDE_BAR.MIN_WIDTH); + + useEffect(() => { + // x 범위 넘어가면 x 위치 조정 + const sidebarWidth = getSidebarWidth(); + if (position.x > window.innerWidth - size.width - sidebarWidth - PADDING) { + // 만약 최대화 상태라면(사이드바 열었을때, 사이드바가 화면을 가린다면), 포지션 0으로 바꾸고 width도 재조정 + // 만약 최대화가 아니라면, 포지션만 조정하고, 사이즈는 그대로 + if (size.width > window.innerWidth - sidebarWidth - PADDING) { + setPosition({ x: 0, y: position.y }); + setSize({ + width: window.innerWidth - sidebarWidth - PADDING, + height: size.height, + }); + } else { + setPosition({ + x: position.x - sidebarWidth + PADDING, + y: position.y, + }); + setSize({ + width: size.width, + height: size.height, + }); + } + } + }, [isSidebarOpen]); + const pageDrag = (e: React.PointerEvent) => { e.preventDefault(); const startX = e.clientX - position.x; @@ -20,7 +50,7 @@ export const usePage = ({ x, y }: Position) => { const handleDragMove = (e: PointerEvent) => { const newX = Math.max( 0, - Math.min(window.innerWidth - size.width - SIDE_BAR.WIDTH - PADDING, e.clientX - startX), + Math.min(window.innerWidth - size.width - getSidebarWidth() - PADDING, e.clientX - startX), ); const newY = Math.max( 0, @@ -51,7 +81,7 @@ export const usePage = ({ x, y }: Position) => { const newWidth = Math.max( PAGE.MIN_WIDTH, - Math.min(startWidth + deltaX, window.innerWidth - position.x - SIDE_BAR.WIDTH - PADDING), + Math.min(startWidth + deltaX, window.innerWidth - position.x - getSidebarWidth() - PADDING), ); const newHeight = Math.max( @@ -81,7 +111,7 @@ export const usePage = ({ x, y }: Position) => { const pageMaximize = () => { setPosition({ x: 0, y: 0 }); setSize({ - width: window.innerWidth - SIDE_BAR.WIDTH - PADDING, + width: window.innerWidth - getSidebarWidth() - PADDING, height: window.innerHeight - PADDING, }); }; diff --git a/client/src/stores/useSidebarStore.ts b/client/src/stores/useSidebarStore.ts new file mode 100644 index 00000000..59d8f220 --- /dev/null +++ b/client/src/stores/useSidebarStore.ts @@ -0,0 +1,18 @@ +import { create } from "zustand"; + +interface SidebarStore { + isSidebarOpen: boolean; + actions: { + toggleSidebar: () => void; + }; +} + +const useSidebarStore = create((set) => ({ + isSidebarOpen: true, + actions: { + toggleSidebar: () => set((state) => ({ isSidebarOpen: !state.isSidebarOpen })), + }, +})); + +export const useIsSidebarOpen = () => useSidebarStore((state) => state.isSidebarOpen); +export const useSidebarActions = () => useSidebarStore((state) => state.actions); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25a92e6b..c8d2ffa7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -79,6 +79,9 @@ importers: socket.io-client: specifier: ^4.8.1 version: 4.8.1 + zustand: + specifier: ^5.0.1 + version: 5.0.1(@types/react@18.3.12)(react@18.3.1) devDependencies: '@pandabox/prettier-plugin': specifier: ^0.1.3 @@ -4572,6 +4575,24 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zustand@5.0.1: + resolution: {integrity: sha512-pRET7Lao2z+n5R/HduXMio35TncTlSW68WsYBq2Lg1ASspsNGjpwLAsij3RpouyV6+kHMwwwzP0bZPD70/Jx/w==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@ampproject/remapping@2.3.0': @@ -9747,3 +9768,8 @@ snapshots: yn@3.1.1: {} yocto-queue@0.1.0: {} + + zustand@5.0.1(@types/react@18.3.12)(react@18.3.1): + optionalDependencies: + '@types/react': 18.3.12 + react: 18.3.1