From 12af2fbbe87e45e99916f9486dd5562a9a760b7a Mon Sep 17 00:00:00 2001 From: geha Date: Thu, 4 Sep 2025 15:59:20 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20dropdown=20menu=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20UI=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ui/dashboard-header/index.tsx | 124 ++++++++++--------- src/components/ui/dropdown/index.tsx | 28 +++++ src/components/ui/dropdown/item.tsx | 18 +++ src/components/ui/dropdown/list.tsx | 23 ++++ src/components/ui/dropdown/toggle.tsx | 31 +++++ src/pages/dashboard/[dashboardId].tsx | 25 ---- src/styles/globals.css | 13 +- 7 files changed, 171 insertions(+), 91 deletions(-) create mode 100644 src/components/ui/dropdown/index.tsx create mode 100644 src/components/ui/dropdown/item.tsx create mode 100644 src/components/ui/dropdown/list.tsx create mode 100644 src/components/ui/dropdown/toggle.tsx delete mode 100644 src/pages/dashboard/[dashboardId].tsx diff --git a/src/components/ui/dashboard-header/index.tsx b/src/components/ui/dashboard-header/index.tsx index 80e6bca..0690576 100644 --- a/src/components/ui/dashboard-header/index.tsx +++ b/src/components/ui/dashboard-header/index.tsx @@ -8,77 +8,74 @@ import { useRef, useState, } from 'react'; +import InviteMemberModal from '@/components/mydashboard/invite-member-modal'; import ChipProfile from '@/components/ui/chip/chip-profile'; import ProfileList from '@/components/ui/dashboard-header/profile-list'; -import InviteMemberModal from '@/components/mydashboard/invite-member-modal'; +import Dropdown from '../dropdown'; const buttonClass = 'flex-center border-gray-3 text-md mobile:px-3 mobile:py-1.5 h-9 cursor-pointer gap-2 rounded-lg border-1 px-4 py-2.5 hover:bg-gray-4 active:bg-gray-3'; export default function DashboardHeader(): ReactNode { - const [isModalOpen, setIsModalOpen] = useState(false); + // const [isModalOpen, setIsModalOpen] = useState(false); const router = useRouter(); - const [open, setOpen] = useState(false); - const menuRef = useRef(null); + // const [open, setOpen] = useState(false); + // const menuRef = useRef(null); + const dashboardId = 1; - const handleOpenModal = () => { - setIsModalOpen(true); - }; - const handleCloseModal = () => { - setIsModalOpen(false); - }; + // const handleOpenModal = () => { + // setIsModalOpen(true); + // }; + + // const handleCloseModal = () => { + // setIsModalOpen(false); + // }; - const handleSubmitInviteMember = () => { - handleCloseModal(); - }; + // const handleSubmitInviteMember = () => { + // handleCloseModal(); + // }; - const toggle = useCallback(() => { - setOpen((v) => !v); - }, []); - const close = useCallback(() => { - setOpen(false); - }, []); + // const toggle = useCallback(() => { + // setOpen((v) => !v); + // }, []); + // const close = useCallback(() => { + // setOpen(false); + // }, []); - useEffect(() => { - const handleDocClick = (e: MouseEvent) => { - if (!menuRef.current) { - return; - } - if (!menuRef.current.contains(e.target as Node)) { - setOpen(false); - } - }; + // useEffect(() => { + // const handleDocClick = (e: MouseEvent) => { + // if (!menuRef.current) { + // return; + // } + // if (!menuRef.current.contains(e.target as Node)) { + // setOpen(false); + // } + // }; - document.addEventListener('mousedown', handleDocClick); + // document.addEventListener('mousedown', handleDocClick); - return () => { - document.removeEventListener('mousedown', handleDocClick); - }; - }, []); + // return () => { + // document.removeEventListener('mousedown', handleDocClick); + // }; + // }, []); - // 이재준 작성 - 내 정보 페이지로 이동하는 기능 추가 - const goMyPage = useCallback(() => { - close(); + const handleMyPageButton = useCallback(() => { router.push('/mypage'); - }, [close, router]); + }, [router]); - // 이재준 작성 - 내 대시보드 페이지로 이동하는 기능 추가 - const goMyDashboard = useCallback(() => { - close(); + const handleMyDashboardButton = useCallback(() => { router.push('/mydashboard'); - }, [close, router]); + }, [router]); - // 이재준 작성 - 로그아웃 기능 추가 - const doLogout = useCallback(async () => { + const handleLogoutButton = useCallback(async () => { try { await fetch('/api/logout', { method: 'POST' }); } catch { // 로그아웃 실패 시 무시 } - close(); router.push('/'); - }, [close, router]); + }, [router]); return (
@@ -104,7 +101,7 @@ export default function DashboardHeader(): ReactNode { 관리 -
); diff --git a/src/components/ui/dropdown/index.tsx b/src/components/ui/dropdown/index.tsx new file mode 100644 index 0000000..6630f85 --- /dev/null +++ b/src/components/ui/dropdown/index.tsx @@ -0,0 +1,28 @@ +import { createContext, type ReactNode, useState } from 'react'; +import Item from '@/components/ui/dropdown/item'; +import List from '@/components/ui/dropdown/list'; +import Toggle from '@/components/ui/dropdown/toggle'; + +interface ContextType { + isOpen: boolean; + setIsOpen: React.Dispatch>; +} +export const DropdownContext = createContext(null); + +export default function Dropdown({ + children, +}: { + children: ReactNode; +}): ReactNode { + const [isOpen, setIsOpen] = useState(false); + + return ( + + {children} + + ); +} + +Dropdown.Toggle = Toggle; +Dropdown.List = List; +Dropdown.Item = Item; diff --git a/src/components/ui/dropdown/item.tsx b/src/components/ui/dropdown/item.tsx new file mode 100644 index 0000000..6cececa --- /dev/null +++ b/src/components/ui/dropdown/item.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from 'react'; + +interface DropdownItemProps { + children: ReactNode; + onClick: () => void; +} +export default function Item({ + children, + onClick, +}: DropdownItemProps): ReactNode { + return ( +
  • + +
  • + ); +} diff --git a/src/components/ui/dropdown/list.tsx b/src/components/ui/dropdown/list.tsx new file mode 100644 index 0000000..06eb60a --- /dev/null +++ b/src/components/ui/dropdown/list.tsx @@ -0,0 +1,23 @@ +import { type ReactNode, useContext } from 'react'; +import { DropdownContext } from '@/components/ui/dropdown'; + +export default function List({ children }: { children: ReactNode }): ReactNode { + const context = useContext(DropdownContext); + + if (!context) { + throw new Error('DropdownContext 내부에서 호출하세요.'); + } + const { isOpen } = context; + + return ( + isOpen && ( +
      + {children} +
    + ) + ); +} diff --git a/src/components/ui/dropdown/toggle.tsx b/src/components/ui/dropdown/toggle.tsx new file mode 100644 index 0000000..c9050e2 --- /dev/null +++ b/src/components/ui/dropdown/toggle.tsx @@ -0,0 +1,31 @@ +import { type ReactNode, useContext } from 'react'; +import { DropdownContext } from '@/components/ui/dropdown'; + +export default function Toggle({ + children, +}: { + children: ReactNode; +}): ReactNode { + const context = useContext(DropdownContext); + + if (!context) { + throw new Error('Dropdown Context 내부에서 호출해야 합니다'); + } + const { isOpen, setIsOpen } = context; + const handleToggle = () => { + setIsOpen((prev) => !prev); + }; + + return ( +
    + {children} +
    + ); +} diff --git a/src/pages/dashboard/[dashboardId].tsx b/src/pages/dashboard/[dashboardId].tsx deleted file mode 100644 index 2362937..0000000 --- a/src/pages/dashboard/[dashboardId].tsx +++ /dev/null @@ -1,25 +0,0 @@ -import type { GetServerSideProps } from 'next'; - -/** - * 이재준 작성 - 인증되지 않은 사용자가 대시보드 페이지에 접근할 때 로그인 페이지로 리다이렉트하기 위해 추가 - */ -export const getServerSideProps: GetServerSideProps = async (context) => { - const { req } = context; - - const accessToken = req.cookies.access_token; - - if (!accessToken) { - return { - redirect: { - destination: `/login`, - permanent: false, - }, - }; - } - - return { props: {} }; -}; - -export default function DashboardPage(): null { - return null; -} diff --git a/src/styles/globals.css b/src/styles/globals.css index da25932..568c8ab 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -76,15 +76,6 @@ --color-background: var(--background); --color-foreground: var(--foreground); } - -html, -body { - height: 100%; - margin: 0; - padding: 0; - overflow-x: hidden; -} - @utility flex-center { display: flex; align-items: center; @@ -101,7 +92,9 @@ body { padding: 0; overflow-x: hidden; } - +button { + cursor: pointer; +} .scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; From 1151d1e57e6c253addfa53665a2e8416db0e34ce Mon Sep 17 00:00:00 2001 From: geha Date: Thu, 4 Sep 2025 17:37:03 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=EB=B0=94=EA=B9=A5=20=ED=81=B4?= =?UTF-8?q?=EB=A6=AD=EC=8B=9C=EC=97=90=20callback=20=EC=8B=A4=ED=96=89?= =?UTF-8?q?=ED=95=98=EB=8A=94=20hook=20=EC=B6=94=EA=B0=80,=20invite-member?= =?UTF-8?q?-modal=EC=9D=84dashboard-header=ED=8F=B4=EB=8D=94=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eslint.config.ts | 3 + src/components/ui/chip/chip-profile.tsx | 4 +- .../ui/dashboard-header/header-dropdown.tsx | 61 ++++++++++ src/components/ui/dashboard-header/index.tsx | 115 ++++-------------- .../dashboard-header}/invite-member-modal.tsx | 0 src/components/ui/dropdown/index.tsx | 9 +- src/components/ui/dropdown/item.tsx | 7 +- src/components/ui/dropdown/list.tsx | 2 +- src/components/ui/dropdown/toggle.tsx | 1 + src/hooks/useExitWhenClickOutSide.ts | 27 ++++ src/pages/dashboard/[dashboardId]/edit.tsx | 2 +- 11 files changed, 132 insertions(+), 99 deletions(-) create mode 100644 src/components/ui/dashboard-header/header-dropdown.tsx rename src/components/{mydashboard => ui/dashboard-header}/invite-member-modal.tsx (100%) create mode 100644 src/hooks/useExitWhenClickOutSide.ts diff --git a/eslint.config.ts b/eslint.config.ts index 02fed17..7227215 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -35,6 +35,7 @@ export default tseslint.config(sheriff(sheriffOptions), { patterns: ['.*'], }, ], + '@typescript-eslint/no-unused-vars': 'warn', '@typescript-eslint/no-floating-promises': 'off', 'react-refresh/only-export-components': 'off', 'react/jsx-no-useless-fragment': 'off', @@ -46,5 +47,7 @@ export default tseslint.config(sheriff(sheriffOptions), { 'react/no-multi-comp': 'off', '@typescript-eslint/no-use-before-define': 'off', '@typescript-eslint/require-await': 'off', + 'tsdoc/syntax': 'off', + 'fsecond/valid-event-listener': 'off', }, }); diff --git a/src/components/ui/chip/chip-profile.tsx b/src/components/ui/chip/chip-profile.tsx index de95c60..4c688a5 100644 --- a/src/components/ui/chip/chip-profile.tsx +++ b/src/components/ui/chip/chip-profile.tsx @@ -26,7 +26,7 @@ export const ChipVariants = cva( }, } ); -interface ChipProps +export interface ChipProfileProps extends HTMLAttributes, VariantProps { children?: ReactNode; @@ -47,7 +47,7 @@ export default function ChipProfile({ size = 'md', color = 'green', ...props -}: ChipProps): ReactNode { +}: ChipProfileProps): ReactNode { return ( <>
    diff --git a/src/components/ui/dashboard-header/header-dropdown.tsx b/src/components/ui/dashboard-header/header-dropdown.tsx new file mode 100644 index 0000000..f8ec4b5 --- /dev/null +++ b/src/components/ui/dashboard-header/header-dropdown.tsx @@ -0,0 +1,61 @@ +import { useRouter } from 'next/router'; +import { type ReactNode, useCallback, useEffect } from 'react'; +import ChipProfile, { + type ChipProfileProps, +} from '@/components/ui/chip/chip-profile'; +import Dropdown from '@/components/ui/dropdown'; + +interface HeaderDropdownProps { + nickname: string; + profileLabel: string; + profileColor: ChipProfileProps['color']; +} +export default function HeaderDropdown({ + nickname, + profileLabel, + profileColor, +}: HeaderDropdownProps): ReactNode { + const router = useRouter(); + + const handleMyPageButton = useCallback(() => { + router.push('/mypage'); + }, [router]); + + const handleMyDashboardButton = useCallback(() => { + router.push('/mydashboard'); + }, [router]); + + const handleLogoutButton = useCallback(async () => { + try { + await fetch('/api/logout', { method: 'POST' }); + } catch { + /** + * @todo + */ + } + router.push('/'); + }, [router]); + + useEffect(() => { + router.prefetch('/mypage'); + router.prefetch('/mydashboard'); + }, [router]); + + return ( + + +
    + + {nickname} +
    +
    + + 로그아웃 + 내 정보 + + 내 대시보드 + + +
    + ); +} diff --git a/src/components/ui/dashboard-header/index.tsx b/src/components/ui/dashboard-header/index.tsx index 0690576..d2019ab 100644 --- a/src/components/ui/dashboard-header/index.tsx +++ b/src/components/ui/dashboard-header/index.tsx @@ -1,81 +1,28 @@ import Image from 'next/image'; import Link from 'next/link'; -import { useRouter } from 'next/router'; -import { - type ReactNode, - useCallback, - useEffect, - useRef, - useState, -} from 'react'; -import InviteMemberModal from '@/components/mydashboard/invite-member-modal'; -import ChipProfile from '@/components/ui/chip/chip-profile'; +import { type ReactNode, useState } from 'react'; +import HeaderDropdown from '@/components/ui/dashboard-header/header-dropdown'; +import InviteMemberModal from '@/components/ui/dashboard-header/invite-member-modal'; import ProfileList from '@/components/ui/dashboard-header/profile-list'; -import Dropdown from '../dropdown'; const buttonClass = 'flex-center border-gray-3 text-md mobile:px-3 mobile:py-1.5 h-9 cursor-pointer gap-2 rounded-lg border-1 px-4 py-2.5 hover:bg-gray-4 active:bg-gray-3'; export default function DashboardHeader(): ReactNode { - // const [isModalOpen, setIsModalOpen] = useState(false); - const router = useRouter(); - // const [open, setOpen] = useState(false); - // const menuRef = useRef(null); - + const [isModalOpen, setIsModalOpen] = useState(false); const dashboardId = 1; - // const handleOpenModal = () => { - // setIsModalOpen(true); - // }; - - // const handleCloseModal = () => { - // setIsModalOpen(false); - // }; - - // const handleSubmitInviteMember = () => { - // handleCloseModal(); - // }; - - // const toggle = useCallback(() => { - // setOpen((v) => !v); - // }, []); - // const close = useCallback(() => { - // setOpen(false); - // }, []); - - // useEffect(() => { - // const handleDocClick = (e: MouseEvent) => { - // if (!menuRef.current) { - // return; - // } - // if (!menuRef.current.contains(e.target as Node)) { - // setOpen(false); - // } - // }; + const handleOpenModal = () => { + setIsModalOpen(true); + }; - // document.addEventListener('mousedown', handleDocClick); + const handleCloseModal = () => { + setIsModalOpen(false); + }; - // return () => { - // document.removeEventListener('mousedown', handleDocClick); - // }; - // }, []); - - const handleMyPageButton = useCallback(() => { - router.push('/mypage'); - }, [router]); - - const handleMyDashboardButton = useCallback(() => { - router.push('/mydashboard'); - }, [router]); - - const handleLogoutButton = useCallback(async () => { - try { - await fetch('/api/logout', { method: 'POST' }); - } catch { - // 로그아웃 실패 시 무시 - } - router.push('/'); - }, [router]); + const handleSubmitInviteMember = () => { + handleCloseModal(); + }; return (
    @@ -101,7 +48,7 @@ export default function DashboardHeader(): ReactNode { 관리 -
    - - -
    - - 권수형 -
    -
    - - - 로그아웃 - - - 내 정보 - - - 내 대시보드 - - -
    +
    + +
    - {/* */} + /> ); diff --git a/src/components/mydashboard/invite-member-modal.tsx b/src/components/ui/dashboard-header/invite-member-modal.tsx similarity index 100% rename from src/components/mydashboard/invite-member-modal.tsx rename to src/components/ui/dashboard-header/invite-member-modal.tsx diff --git a/src/components/ui/dropdown/index.tsx b/src/components/ui/dropdown/index.tsx index 6630f85..56a6b9d 100644 --- a/src/components/ui/dropdown/index.tsx +++ b/src/components/ui/dropdown/index.tsx @@ -2,6 +2,7 @@ import { createContext, type ReactNode, useState } from 'react'; import Item from '@/components/ui/dropdown/item'; import List from '@/components/ui/dropdown/list'; import Toggle from '@/components/ui/dropdown/toggle'; +import useExitWhenClickOutSide from '@/hooks/useExitWhenClickOutSide'; interface ContextType { isOpen: boolean; @@ -15,10 +16,16 @@ export default function Dropdown({ children: ReactNode; }): ReactNode { const [isOpen, setIsOpen] = useState(false); + const handleClose = () => { + setIsOpen(false); + }; + const dropdownRef = useExitWhenClickOutSide(handleClose); return ( - {children} +
    + {children} +
    ); } diff --git a/src/components/ui/dropdown/item.tsx b/src/components/ui/dropdown/item.tsx index 6cececa..0ac93ff 100644 --- a/src/components/ui/dropdown/item.tsx +++ b/src/components/ui/dropdown/item.tsx @@ -9,8 +9,11 @@ export default function Item({ onClick, }: DropdownItemProps): ReactNode { return ( -
  • -
  • diff --git a/src/components/ui/dropdown/list.tsx b/src/components/ui/dropdown/list.tsx index 06eb60a..720da19 100644 --- a/src/components/ui/dropdown/list.tsx +++ b/src/components/ui/dropdown/list.tsx @@ -14,7 +14,7 @@ export default function List({ children }: { children: ReactNode }): ReactNode {
      {children}
    diff --git a/src/components/ui/dropdown/toggle.tsx b/src/components/ui/dropdown/toggle.tsx index c9050e2..f57ead0 100644 --- a/src/components/ui/dropdown/toggle.tsx +++ b/src/components/ui/dropdown/toggle.tsx @@ -17,6 +17,7 @@ export default function Toggle({ }; return ( + // eslint-disable-next-line jsx-a11y/click-events-have-key-events
    void +): Ref { + const ref = useRef(null); + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + console.log(ref.current, e.target); + if (!ref.current) { + return; + } + if (!ref.current.contains(e.target as Node)) { + callback(); + } + }; + + document.addEventListener('click', handleClickOutside); + + return () => { + document.removeEventListener('click', handleClickOutside); + }; + }, [callback]); + + return ref; +} diff --git a/src/pages/dashboard/[dashboardId]/edit.tsx b/src/pages/dashboard/[dashboardId]/edit.tsx index d731b83..6fdff5f 100644 --- a/src/pages/dashboard/[dashboardId]/edit.tsx +++ b/src/pages/dashboard/[dashboardId]/edit.tsx @@ -4,7 +4,7 @@ import Link from 'next/link'; import { useRouter } from 'next/router'; import { type ReactNode, useState } from 'react'; import DashboardLayout from '@/components/layout/dashboard-layout'; -import InviteMemberModal from '@/components/mydashboard/invite-member-modal'; +import InviteMemberModal from '@/components/ui/dashboard-header/invite-member-modal'; export default function MydashboardEdit(): ReactNode { const router = useRouter(); From acefdec9f971ea69a46e71d0a6694b57b4256237 Mon Sep 17 00:00:00 2001 From: geha Date: Thu, 4 Sep 2025 18:23:33 +0900 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20header=20dropdown=20menu=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/dashboard-header/header-dropdown.tsx | 7 +++++-- src/components/ui/dashboard-header/index.tsx | 12 +++++------- src/components/ui/dropdown/index.tsx | 3 ++- src/components/ui/dropdown/list.tsx | 19 ++++++++++++++++--- src/components/ui/dropdown/toggle.tsx | 2 +- 5 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/components/ui/dashboard-header/header-dropdown.tsx b/src/components/ui/dashboard-header/header-dropdown.tsx index f8ec4b5..3ad790b 100644 --- a/src/components/ui/dashboard-header/header-dropdown.tsx +++ b/src/components/ui/dashboard-header/header-dropdown.tsx @@ -44,12 +44,15 @@ export default function HeaderDropdown({ return ( -
    +
    {nickname}
    - + 로그아웃 내 정보 diff --git a/src/components/ui/dashboard-header/index.tsx b/src/components/ui/dashboard-header/index.tsx index d2019ab..296a0bc 100644 --- a/src/components/ui/dashboard-header/index.tsx +++ b/src/components/ui/dashboard-header/index.tsx @@ -57,13 +57,11 @@ export default function DashboardHeader(): ReactNode {
    -
    - -
    +
    >; } + export const DropdownContext = createContext(null); export default function Dropdown({ @@ -23,7 +24,7 @@ export default function Dropdown({ return ( -
    +
    {children}
    diff --git a/src/components/ui/dropdown/list.tsx b/src/components/ui/dropdown/list.tsx index 720da19..85b033e 100644 --- a/src/components/ui/dropdown/list.tsx +++ b/src/components/ui/dropdown/list.tsx @@ -1,7 +1,17 @@ import { type ReactNode, useContext } from 'react'; import { DropdownContext } from '@/components/ui/dropdown'; +import { cn } from '@/utils/cn'; -export default function List({ children }: { children: ReactNode }): ReactNode { +interface DropdownListProps { + children: ReactNode; + positionClassName?: string; + ariaLabel?: string; +} +export default function List({ + children, + positionClassName, + ariaLabel = '메뉴', +}: DropdownListProps): ReactNode { const context = useContext(DropdownContext); if (!context) { @@ -13,8 +23,11 @@ export default function List({ children }: { children: ReactNode }): ReactNode { isOpen && (
      {children}
    diff --git a/src/components/ui/dropdown/toggle.tsx b/src/components/ui/dropdown/toggle.tsx index f57ead0..1eb2164 100644 --- a/src/components/ui/dropdown/toggle.tsx +++ b/src/components/ui/dropdown/toggle.tsx @@ -23,7 +23,7 @@ export default function Toggle({ tabIndex={0} aria-haspopup='menu' aria-expanded={isOpen} - className='flex-center hover:bg-gray-4 active:bg-gray-3 relative w-full' + className='flex-center relative h-full w-full' onClick={handleToggle} > {children} From d9804f5ba38b7285dd7f6960ab4184ea1fd28af7 Mon Sep 17 00:00:00 2001 From: geha Date: Thu, 4 Sep 2025 18:45:49 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20dropdown=20story=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/stories/Dropdown.stories.tsx | 72 ++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/stories/Dropdown.stories.tsx diff --git a/src/stories/Dropdown.stories.tsx b/src/stories/Dropdown.stories.tsx new file mode 100644 index 0000000..1faab6a --- /dev/null +++ b/src/stories/Dropdown.stories.tsx @@ -0,0 +1,72 @@ +import type { ComponentType } from 'react'; +import { fn } from 'storybook/test'; +import { INITIAL_VIEWPORTS, MINIMAL_VIEWPORTS } from 'storybook/viewport'; +import type { Meta, StoryObj } from '@storybook/nextjs-vite'; +import Dropdown from '@/components/ui/dropdown'; + +const meta = { + title: 'Dropdown', + component: Dropdown, + subcomponents: { + Toggle: Dropdown.Toggle as ComponentType, + List: Dropdown.List as ComponentType, + Item: Dropdown.Item as ComponentType, + }, + decorators: [ + (Story) => { + return ( +
    + +
    + ); + }, + ], + parameters: { + viewport: { ...MINIMAL_VIEWPORTS, ...INITIAL_VIEWPORTS }, + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: {}, + args: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { children: <> }, + parameters: { + docs: { + canvas: { + sourceState: 'shown', + }, + }, + }, + render: () => { + function DropdownStory() { + /** + * 코드 예시: return 괄호 내부 + */ + return ( + + +
    + 내 정보 +
    +
    + + 0}>로그아웃 + 0}>내 정보 + 0}>내 대시보드 + +
    + ); + /* 끝 */ + } + + return ; + }, +}; From 09fcef01ec2f1abe363b4dda0a80b5c70cf17dfc Mon Sep 17 00:00:00 2001 From: geha Date: Fri, 5 Sep 2025 10:58:48 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20getServerSideProps=20query=EC=9A=A9?= =?UTF-8?q?=20util=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eslint.config.ts | 3 +++ src/hooks/useExitWhenClickOutSide.ts | 1 - src/utils/getContextQuery.ts | 29 ++++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 src/utils/getContextQuery.ts diff --git a/eslint.config.ts b/eslint.config.ts index 7227215..03a898c 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -49,5 +49,8 @@ export default tseslint.config(sheriff(sheriffOptions), { '@typescript-eslint/require-await': 'off', 'tsdoc/syntax': 'off', 'fsecond/valid-event-listener': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + 'fsecond/prefer-destructured-optionals': 'off', + '@typescript-eslint/no-misused-spread': 'off', }, }); diff --git a/src/hooks/useExitWhenClickOutSide.ts b/src/hooks/useExitWhenClickOutSide.ts index ee7f3eb..68408df 100644 --- a/src/hooks/useExitWhenClickOutSide.ts +++ b/src/hooks/useExitWhenClickOutSide.ts @@ -7,7 +7,6 @@ export default function useExitWhenClickOutSide( useEffect(() => { const handleClickOutside = (e: MouseEvent) => { - console.log(ref.current, e.target); if (!ref.current) { return; } diff --git a/src/utils/getContextQuery.ts b/src/utils/getContextQuery.ts new file mode 100644 index 0000000..8abb085 --- /dev/null +++ b/src/utils/getContextQuery.ts @@ -0,0 +1,29 @@ +import type { ParsedUrlQuery } from 'node:querystring'; +/** + * 쿼리 파라미터에서 숫자 ID를 안정적인 타입으로 추출합니다 + */ +export function getNumberFromQuery( + query: ParsedUrlQuery, + key: string +): number | null { + const value = query[key]; + + if (!value || Array.isArray(value)) { + return null; + } + const num = parseInt(value, 10); + + return Number.isNaN(num) ? null : num; +} + +/** + * 쿼리 파라미터에서 문자열을 안정적인 타입으로 추출합니다 + */ +export function getStringFromQuery( + query: ParsedUrlQuery, + key: string +): string | null { + const value = query[key]; + + return typeof value === 'string' ? value : null; +}