diff --git a/package.json b/package.json index c47c358b..12d342f8 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@tanstack/react-query": "^5.49.2", "@tanstack/react-query-devtools": "^5.49.2", "axios": "^1.7.2", + "framer-motion": "^11.11.9", "mime": "^4.0.4", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c2e00458..a00f42ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,6 +28,9 @@ importers: axios: specifier: ^1.7.2 version: 1.7.2 + framer-motion: + specifier: ^11.11.9 + version: 11.11.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) mime: specifier: ^4.0.4 version: 4.0.4 @@ -2837,6 +2840,20 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} + framer-motion@11.11.9: + resolution: {integrity: sha512-XpdZseuCrZehdHGuW22zZt3SF5g6AHJHJi7JwQIigOznW4Jg1n0oGPMJQheMaKLC+0rp5gxUKMRYI6ytd3q4RQ==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 + react-dom: ^18.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} @@ -7887,6 +7904,13 @@ snapshots: forwarded@0.2.0: {} + framer-motion@11.11.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + tslib: 2.6.3 + optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + fresh@0.5.2: {} fs-extra@11.2.0: diff --git a/src/App.tsx b/src/App.tsx index 4a62dec8..74fdba75 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/react'; import * as Sentry from '@sentry/react'; -import { Outlet, useLocation, useNavigate } from 'react-router-dom'; +import { Outlet, useNavigate } from 'react-router-dom'; import { useQueryErrorResetBoundary } from '@tanstack/react-query'; @@ -21,11 +21,6 @@ const App = () => { const { reset } = useQueryErrorResetBoundary(); - const { pathname } = useLocation(); - - /** 아카이빙 페이지 DocumentBar를 위한 라우트별 동적 패딩 */ - const isArchivingPage = pathname === '/archiving'; - Sentry.init({ dsn: import.meta.env.VITE_SENTRY_DSN, integrations: [Sentry.browserTracingIntegration()], @@ -55,7 +50,7 @@ const App = () => { -
+
@@ -64,19 +59,18 @@ const App = () => { ); }; -const layoutStyle = (flag: boolean) => - css({ - display: 'flex', - flexDirection: 'column', +const layoutStyle = css({ + display: 'flex', + flexDirection: 'column', - height: '100%', - width: 'calc(100% - 7.6rem)', + height: '100%', + width: 'calc(100% - 7.6rem)', - padding: flag ? '0' : '2rem 3.4rem 4.8rem 3.2rem', + padding: '2rem 3.4rem 4.8rem 3.2rem', - marginLeft: '7.6rem', + marginLeft: '7.6rem', - overflow: 'hidden', - }); + overflow: 'hidden', +}); export default App; diff --git a/src/common/asset/svg/ic_folder_copy.svg b/src/common/asset/svg/ic_folder_copy.svg new file mode 100644 index 00000000..fcca5d64 --- /dev/null +++ b/src/common/asset/svg/ic_folder_copy.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/asset/svg/ic_handover_empty.svg b/src/common/asset/svg/ic_handover_empty.svg new file mode 100644 index 00000000..c1e3b788 --- /dev/null +++ b/src/common/asset/svg/ic_handover_empty.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/asset/svg/ic_nav_timeline.svg b/src/common/asset/svg/ic_nav_timeline.svg new file mode 100644 index 00000000..f9e40e09 --- /dev/null +++ b/src/common/asset/svg/ic_nav_timeline.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/common/router/Router.tsx b/src/common/router/Router.tsx index 9b2a05e8..b278a66d 100644 --- a/src/common/router/Router.tsx +++ b/src/common/router/Router.tsx @@ -141,6 +141,14 @@ const router = createBrowserRouter([ ), }, + { + path: PATH.HANDOVER, + element: ( + +

HandOver

+
+ ), + }, ], }, ]); diff --git a/src/page/archiving/index/ArchivingPage.tsx b/src/page/archiving/index/ArchivingPage.tsx index ad88128f..b41b6049 100644 --- a/src/page/archiving/index/ArchivingPage.tsx +++ b/src/page/archiving/index/ArchivingPage.tsx @@ -1,5 +1,4 @@ import { Suspense } from 'react'; -import { useLocation } from 'react-router-dom'; import Button from '@/common/component/Button/Button'; import Flex from '@/common/component/Flex/Flex'; @@ -16,19 +15,15 @@ import ContentBox from '@/shared/component/ContentBox/ContentBox'; import { useOpenModal } from '@/shared/store/modal'; const ArchivingPage = () => { - const location = useLocation(); - const sideBarRef = useOutsideClick(() => setSelectedBlock(undefined)); const { selectedBlock, setSelectedBlock, handleBlockClick } = useInteractTimeline(); const openModal = useOpenModal(); - const teamId = new URLSearchParams(location.search).get('teamId'); - - if (!teamId) throw new Error('has no teamId'); + const teamId = localStorage.getItem('teamId'); - const { ref, currentYear, currentMonth, handlePrevMonth, handleNextMonth, handleToday, endDay } = useDate(teamId); + const { ref, currentYear, currentMonth, handlePrevMonth, handleNextMonth, handleToday, endDay } = useDate(teamId!); const handleOpenBlockModal = () => { openModal('create-block'); @@ -40,7 +35,7 @@ const ArchivingPage = () => { variant="timeline" title="타임라인" headerOption={ - } diff --git a/src/page/archiving/index/component/DocumentBar/DocumentBar.style.ts b/src/page/archiving/index/component/DocumentBar/DocumentBar.style.ts index a61efc0f..61525457 100644 --- a/src/page/archiving/index/component/DocumentBar/DocumentBar.style.ts +++ b/src/page/archiving/index/component/DocumentBar/DocumentBar.style.ts @@ -4,7 +4,8 @@ import { theme } from '@/common/style/theme/theme'; export const containerStyle = (blockSelected: string) => css({ - position: 'relative', + position: 'absolute', + right: 0, zIndex: theme.zIndex.overlayMiddle, diff --git a/src/page/archiving/index/component/TimeLine/index.tsx b/src/page/archiving/index/component/TimeLine/index.tsx index 0a77a471..368ba03f 100644 --- a/src/page/archiving/index/component/TimeLine/index.tsx +++ b/src/page/archiving/index/component/TimeLine/index.tsx @@ -19,10 +19,9 @@ const TimeLine = ( { selectedBlock, onBlockClick, currentYear, currentMonth, endDay }: TimeLineProps, ref: ForwardedRef ) => { - const teamId = new URLSearchParams(location.search).get('teamId'); - if (!teamId) throw new Error('has no teamId'); + const teamId = localStorage.getItem('teamId'); - const { data } = useGetTimeBlockQuery(+teamId, 'executive', currentYear, currentMonth); + const { data } = useGetTimeBlockQuery(+teamId!, 'executive', currentYear, currentMonth); const timeBlocks: Block[] = data.timeBlocks; const blockFloors = alignBlocks(timeBlocks, endDay, currentMonth, currentYear); diff --git a/src/shared/component/ContentBox/ContentBox.style.ts b/src/shared/component/ContentBox/ContentBox.style.ts index fbd0230a..97986d12 100644 --- a/src/shared/component/ContentBox/ContentBox.style.ts +++ b/src/shared/component/ContentBox/ContentBox.style.ts @@ -6,10 +6,11 @@ export const sectionStyle = css({ position: 'relative', width: '100%', - height: 'calc(100vh - 4.8rem - 6rem)', - padding: '1.6rem', - paddingTop: '0', + minHeight: 'calc(100vh - 11.6rem - 4.8rem - 2rem)', + height: 'calc(100vh - 11.6rem - 4.8rem - 2rem)', + + padding: '0 1.6rem', border: `1px solid ${theme.colors.gray_300}`, borderRadius: '16px', @@ -50,6 +51,8 @@ export const headerStyle = css({ position: 'sticky', top: 0, + height: '7.2rem', + zIndex: theme.zIndex.overlayBottom, padding: '1.6rem 0rem', @@ -68,7 +71,7 @@ export const contentOptionStyle = css({ export const contentStyle = css({ width: '100%', - height: 'calc(100% - 12rem)', + height: '100%', marginTop: '0.8rem', diff --git a/src/shared/component/Header/Header.style.ts b/src/shared/component/Header/Header.style.ts index 1903eac9..c7db61d0 100644 --- a/src/shared/component/Header/Header.style.ts +++ b/src/shared/component/Header/Header.style.ts @@ -3,6 +3,10 @@ import { css } from '@emotion/react'; import { theme } from '@/common/style/theme/theme'; export const headerStyle = css({ + display: 'flex', + flexDirection: 'column', + gap: '2rem', + width: 'fit-content', paddingBottom: '2rem', diff --git a/src/shared/component/Header/Header.tsx b/src/shared/component/Header/Header.tsx index 2d3037c2..2a3b04b9 100644 --- a/src/shared/component/Header/Header.tsx +++ b/src/shared/component/Header/Header.tsx @@ -1,23 +1,18 @@ -import { useLocation } from 'react-router-dom'; - import Heading from '@/common/component/Heading/Heading'; import { headerStyle } from '@/shared/component/Header/Header.style'; +import RouteNav from '@/shared/component/RouteNav/RouteNav'; const Header = () => { /** TODO: 추후 global State 혹은 localStorage에 저장 */ const title = 'TIKI 워크스페이스'; - const { pathname } = useLocation(); - - const hasNoSidebar = pathname !== '/archiving'; - return ( - hasNoSidebar && ( -
- {title} -
- ) +
+ {title} + + +
); }; diff --git a/src/shared/component/RouteNav/RouteNav.style.ts b/src/shared/component/RouteNav/RouteNav.style.ts new file mode 100644 index 00000000..bcc02be8 --- /dev/null +++ b/src/shared/component/RouteNav/RouteNav.style.ts @@ -0,0 +1,63 @@ +import { css } from '@emotion/react'; + +import { theme } from '@/common/style/theme/theme'; + +export const iconFillActiveStyle = (isActive: boolean) => + css({ + '& > path': { + fill: isActive ? theme.colors.gray_800 : theme.colors.gray_500, + }, + }); + +export const iconStrokeActiveStyle = (isActive: boolean) => + css({ + '& > path': { + stroke: isActive ? theme.colors.gray_800 : theme.colors.gray_500, + }, + }); + +export const navListStyle = css({ + display: 'flex', + alignItems: 'center', + gap: '8px', + + width: 'max-content', + + padding: '0.4rem', + + borderRadius: '8px', + backgroundColor: theme.colors.gray_100, +}); + +export const itemStyle = (isActive: boolean) => + css({ + display: 'flex', + alignItems: 'center', + gap: '0.6rem', + + position: 'relative', + padding: '0.6rem 0.8rem', + + backgroundColor: 'transparent', + + ...theme.text.body08, + color: isActive ? theme.colors.black : theme.colors.gray_500, + + whiteSpace: 'nowrap', + + zIndex: 2, + }); + +export const indicatorStyle = css({ + position: 'absolute', + top: -2, + + width: '100%', + height: '3.2rem', + + backgroundColor: theme.colors.white, + + borderRadius: '4px', + + zIndex: 1, +}); diff --git a/src/shared/component/RouteNav/RouteNav.tsx b/src/shared/component/RouteNav/RouteNav.tsx new file mode 100644 index 00000000..e269fb60 --- /dev/null +++ b/src/shared/component/RouteNav/RouteNav.tsx @@ -0,0 +1,54 @@ +import { motion } from 'framer-motion'; + +import { Link, useLocation } from 'react-router-dom'; + +import IcFolder from '@/common/asset/svg/ic_folder_copy.svg?react'; +import IcHandOver from '@/common/asset/svg/ic_handover_empty.svg?react'; +import IcTimeLine from '@/common/asset/svg/ic_nav_timeline.svg?react'; + +import { + iconFillActiveStyle, + iconStrokeActiveStyle, + indicatorStyle, + itemStyle, + navListStyle, +} from '@/shared/component/RouteNav/RouteNav.style'; +import { PATH } from '@/shared/constant/path'; + +const RouteNav = () => { + const { pathname } = useLocation(); + + const isDrivePage = pathname === PATH.DRIVE; + const isArchivingPage = pathname === PATH.ARCHIVING; + const isHandoverPage = pathname === PATH.HANDOVER; + + return ( + + ); +}; + +export default RouteNav; diff --git a/src/shared/component/SideNavBar/LeftSidebar.tsx b/src/shared/component/SideNavBar/LeftSidebar.tsx index c1b9d69d..cf311956 100644 --- a/src/shared/component/SideNavBar/LeftSidebar.tsx +++ b/src/shared/component/SideNavBar/LeftSidebar.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import addUrl from '@/common/asset/svg/ic_add.svg'; @@ -38,34 +38,16 @@ const LeftSidebar = () => { const { isOpen: isSettingOpen, close: onSettingClose, toggle } = useOverlay(); - useEffect(() => { - const searchParams = new URLSearchParams(window.location.search); - const teamId = searchParams.get('teamId'); - if (teamId) { - setSelectedId(teamId); - - navigate(`${PATH.ARCHIVING}?teamId=${teamId}`); - } else { - setSelectedId('showcase'); - navigate(PATH.SHOWCASE); - } - }, [navigate]); - const handleItemClick = (id: string, path: string) => { setSelectedId(id); - const searchParams = new URLSearchParams(window.location.search); - searchParams.set('teamId', id); - - const hasTeamIdInPath = path.includes('teamId'); - - if (!hasTeamIdInPath && id !== 'showcase') { - navigate(`${path}?${searchParams.toString()}`); - } else if (id === 'showcase') { - navigate(PATH.SHOWCASE); + if (id === 'showcase') { + navigate(path); } else { navigate(path); + localStorage.setItem('teamId', id); } + close(); }; @@ -93,7 +75,7 @@ const LeftSidebar = () => { key={data.id} isClicked={selectedId === String(data.id)} logoUrl={data.iconImageUrl ? data.iconImageUrl : defaultLogo} - onClick={() => handleItemClick(String(data.id), `${PATH.ARCHIVING}?teamId=${data.id}`)}> + onClick={() => handleItemClick(String(data.id), PATH.ARCHIVING)}> {data.name} ); diff --git a/src/shared/constant/path.ts b/src/shared/constant/path.ts index acc7ebcd..c023466b 100644 --- a/src/shared/constant/path.ts +++ b/src/shared/constant/path.ts @@ -14,6 +14,7 @@ export const PATH = { ARCHIVING: '/archiving', SHOWCASE: '/showcase', DRIVE: '/drive', + HANDOVER: '/handover', COMING_SOON: '/comingsoon', } as const; diff --git a/src/story/shared/RouteNav.stories.tsx b/src/story/shared/RouteNav.stories.tsx new file mode 100644 index 00000000..343c7c69 --- /dev/null +++ b/src/story/shared/RouteNav.stories.tsx @@ -0,0 +1,17 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import RouteNav from '@/shared/component/RouteNav/RouteNav'; + +const meta: Meta = { + title: 'Shared/RouteNav', + component: RouteNav, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {};