From 983ecc2ccb7ae06b8d7f47522225e10d3451429a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A0=95=EC=9B=90?= Date: Wed, 4 Feb 2026 19:15:39 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EC=82=AC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=B0=94=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sidebar/MobileDrawer.stories.tsx | 50 ++++++++ src/components/sidebar/MobileDrawer.tsx | 96 ++++++++++++++++ .../sidebar/MobileHeader.stories.tsx | 37 ++++++ src/components/sidebar/MobileHeader.tsx | 68 +++++++++++ src/components/sidebar/Sidebar.stories.tsx | 95 +++++++++++++++ src/components/sidebar/Sidebar.tsx | 81 +++++++++++++ .../sidebar/SidebarAddButton.stories.tsx | 30 +++++ src/components/sidebar/SidebarAddButton.tsx | 26 +++++ .../sidebar/SidebarButton.stories.tsx | 79 +++++++++++++ src/components/sidebar/SidebarButton.tsx | 29 +++++ .../sidebar/SidebarTeamSelect.stories.tsx | 64 +++++++++++ src/components/sidebar/SidebarTeamSelect.tsx | 51 +++++++++ src/components/sidebar/index.ts | 7 ++ .../sidebar/styles/MobileDrawer.module.css | 57 +++++++++ .../sidebar/styles/MobileHeader.module.css | 54 +++++++++ .../sidebar/styles/Sidebar.module.css | 108 ++++++++++++++++++ .../styles/SidebarAddButton.module.css | 24 ++++ .../sidebar/styles/SidebarButton.module.css | 56 +++++++++ .../styles/SidebarTeamSelect.module.css | 50 ++++++++ src/components/sidebar/types/types.ts | 14 +++ 20 files changed, 1076 insertions(+) create mode 100644 src/components/sidebar/MobileDrawer.stories.tsx create mode 100644 src/components/sidebar/MobileDrawer.tsx create mode 100644 src/components/sidebar/MobileHeader.stories.tsx create mode 100644 src/components/sidebar/MobileHeader.tsx create mode 100644 src/components/sidebar/Sidebar.stories.tsx create mode 100644 src/components/sidebar/Sidebar.tsx create mode 100644 src/components/sidebar/SidebarAddButton.stories.tsx create mode 100644 src/components/sidebar/SidebarAddButton.tsx create mode 100644 src/components/sidebar/SidebarButton.stories.tsx create mode 100644 src/components/sidebar/SidebarButton.tsx create mode 100644 src/components/sidebar/SidebarTeamSelect.stories.tsx create mode 100644 src/components/sidebar/SidebarTeamSelect.tsx create mode 100644 src/components/sidebar/index.ts create mode 100644 src/components/sidebar/styles/MobileDrawer.module.css create mode 100644 src/components/sidebar/styles/MobileHeader.module.css create mode 100644 src/components/sidebar/styles/Sidebar.module.css create mode 100644 src/components/sidebar/styles/SidebarAddButton.module.css create mode 100644 src/components/sidebar/styles/SidebarButton.module.css create mode 100644 src/components/sidebar/styles/SidebarTeamSelect.module.css create mode 100644 src/components/sidebar/types/types.ts diff --git a/src/components/sidebar/MobileDrawer.stories.tsx b/src/components/sidebar/MobileDrawer.stories.tsx new file mode 100644 index 0000000..13c710f --- /dev/null +++ b/src/components/sidebar/MobileDrawer.stories.tsx @@ -0,0 +1,50 @@ +import type { Meta, StoryObj } from '@storybook/nextjs-vite'; + +import { fn } from 'storybook/test'; +import Image from 'next/image'; + +import MobileDrawer from './MobileDrawer'; +import SidebarButton from './SidebarButton'; +import SidebarAddButton from './SidebarAddButton'; +import chessSmall from '@/assets/icons/chess/chessSmall.svg'; +import boardSmall from '@/assets/icons/board/boardSmall.svg'; + +const TEAMS = ['경영관리팀', '프로덕트팀', '마케팅팀', '콘텐츠팀']; + +const meta = { + title: 'Components/MobileDrawer', + component: MobileDrawer, + parameters: { + layout: 'fullscreen', + viewport: { defaultViewport: 'mobile1' }, + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Open: Story = { + args: { + isOpen: true, + onClose: fn(), + children: ( + <> + {TEAMS.map((team) => ( + } + label={team} + isActive={team === '경영관리팀'} + /> + ))} + +
+ } + label="자유게시판" + /> + + ), + }, +}; diff --git a/src/components/sidebar/MobileDrawer.tsx b/src/components/sidebar/MobileDrawer.tsx new file mode 100644 index 0000000..4cf0ec5 --- /dev/null +++ b/src/components/sidebar/MobileDrawer.tsx @@ -0,0 +1,96 @@ +'use client'; + +import { useEffect, useRef, type ReactNode } from 'react'; +import Image from 'next/image'; + +import styles from './styles/MobileDrawer.module.css'; +import xMarkBig from '@/assets/icons/xMark/xMarkBig.svg'; + +type MobileDrawerProps = { + /** 드로어 열림/닫힘 상태 */ + isOpen: boolean; + /** 드로어 내부에 표시할 콘텐츠 */ + children: ReactNode; + /** 닫기 시 호출되는 콜백 (오버레이 클릭, X 버튼, ESC 키) */ + onClose: () => void; +}; + +/** + * 모바일 사이드 드로어 (슬라이드 패널). + * 오버레이 클릭, X 버튼, ESC 키로 닫을 수 있으며 포커스 트랩이 적용됩니다. + * children에 사이드바 메뉴 등 원하는 콘텐츠를 주입할 수 있습니다. + */ +const FOCUSABLE_SELECTOR = + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; + +export default function MobileDrawer({ isOpen, children, onClose }: MobileDrawerProps) { + const drawerRef = useRef(null); + const focusableRef = useRef([]); + + useEffect(() => { + if (!isOpen || !drawerRef.current) return; + + focusableRef.current = Array.from( + drawerRef.current.querySelectorAll(FOCUSABLE_SELECTOR), + ); + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onClose(); + return; + } + + if (e.key !== 'Tab') return; + + const items = focusableRef.current; + if (items.length === 0) return; + + const first = items[0]; + const last = items[items.length - 1]; + + if (e.shiftKey && document.activeElement === first) { + e.preventDefault(); + last.focus(); + } else if (!e.shiftKey && document.activeElement === last) { + e.preventDefault(); + first.focus(); + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [isOpen, onClose]); + + useEffect(() => { + if (isOpen) { + drawerRef.current?.querySelector('button')?.focus(); + } + }, [isOpen]); + + if (!isOpen) return null; + + return ( + <> +