diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 458e9340..d78b7999 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -23,4 +23,4 @@ export const App = () => ( } /> } /> -); \ No newline at end of file +); diff --git a/frontend/src/component/common/dropdown/Dropdown.tsx b/frontend/src/component/common/dropdown/Dropdown.tsx new file mode 100644 index 00000000..ec3933fd --- /dev/null +++ b/frontend/src/component/common/dropdown/Dropdown.tsx @@ -0,0 +1,35 @@ +import React, { createContext, ReactNode, useMemo, useState } from 'react'; +import { DropdownTrigger } from '@/component/common/dropdown/DropdownTrigger.tsx'; +import { DropdownItem } from '@/component/common/dropdown/DropdownItem.tsx'; +import { DropdownMenu } from '@/component/common/dropdown/DropdownMenu.tsx'; + +interface IDropdownProps { + children: ReactNode; +} + +export interface IToggleContext { + isOpen: boolean; + setIsOpen: React.Dispatch>; +} + +export const ToggleContext = createContext({ + isOpen: false, + setIsOpen: () => {}, +}); + +// Todo : 드랍다운 외부에서 클릭시 창 닫히게 설정 +export const Dropdown = (props: IDropdownProps) => { + const [isOpen, setIsOpen] = useState(false); + + const toggleContextValue = useMemo(() => ({ isOpen, setIsOpen }), [isOpen, setIsOpen]); + + return ( + + ); +}; + +Dropdown.Trigger = DropdownTrigger; +Dropdown.Item = DropdownItem; +Dropdown.Menu = DropdownMenu; diff --git a/frontend/src/component/common/dropdown/DropdownButton.tsx b/frontend/src/component/common/dropdown/DropdownButton.tsx deleted file mode 100644 index 6e5043d9..00000000 --- a/frontend/src/component/common/dropdown/DropdownButton.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { ReactNode } from 'react'; -import classNames from 'classnames'; - -interface IDropdownButtonProps extends React.ButtonHTMLAttributes { - children: ReactNode; - className?: string; -} - -export const DropdownButton = (props: IDropdownButtonProps) => { - return ( - - ); -}; diff --git a/frontend/src/component/common/dropdown/DropdownItem.tsx b/frontend/src/component/common/dropdown/DropdownItem.tsx new file mode 100644 index 00000000..472e372c --- /dev/null +++ b/frontend/src/component/common/dropdown/DropdownItem.tsx @@ -0,0 +1,21 @@ +import { ReactNode } from 'react'; + +interface IDropdownItemProps { + children: ReactNode; + onClick?: React.MouseEventHandler; +} + +export const DropdownItem = (props: IDropdownItemProps) => { + // undefined는 react에서 랜더링 하지 않음 + return ( +
  • + +
  • + ); +}; diff --git a/frontend/src/component/common/dropdown/DropdownMenu.tsx b/frontend/src/component/common/dropdown/DropdownMenu.tsx new file mode 100644 index 00000000..e80c67ac --- /dev/null +++ b/frontend/src/component/common/dropdown/DropdownMenu.tsx @@ -0,0 +1,63 @@ +import { ReactNode, useContext, useRef, useEffect } from 'react'; +import classNames from 'classnames'; +import { ToggleContext } from '@/component/common/dropdown/Dropdown.tsx'; + +interface IDropdownMenuProps { + children: ReactNode | ReactNode[]; +} + +export const DropdownMenu = (props: IDropdownMenuProps) => { + const { isOpen, setIsOpen } = useContext(ToggleContext); + const ref = useRef(null); + + const handleOutSideClick = (event: MouseEvent) => { + const { target } = event; + + if (!(target instanceof HTMLElement)) { + return; + } + + if ( + ref.current && + target && + !ref.current.contains(target) && + target.dataset.component !== 'DropdownTrigger' + ) { + setIsOpen(false); + } + }; + + useEffect(() => { + document.addEventListener('click', handleOutSideClick); + return () => { + document.removeEventListener('click', handleOutSideClick); + }; + }, []); + + return ( + isOpen && ( +
      + {props.children} +
    + ) + ); +}; diff --git a/frontend/src/component/common/dropdown/DropdownTrigger.tsx b/frontend/src/component/common/dropdown/DropdownTrigger.tsx new file mode 100644 index 00000000..75e2d2cc --- /dev/null +++ b/frontend/src/component/common/dropdown/DropdownTrigger.tsx @@ -0,0 +1,33 @@ +import { ReactNode, useContext } from 'react'; +import classNames from 'classnames'; +import { ToggleContext } from '@/component/common/dropdown/Dropdown.tsx'; + +interface IDropdownTriggerProps { + children: ReactNode; +} + +export const DropdownTrigger = (props: IDropdownTriggerProps) => { + const { setIsOpen } = useContext(ToggleContext); + + const handleOnClick = () => { + setIsOpen(prevIsOpen => !prevIsOpen); + }; + + return ( + + ); +}; diff --git a/frontend/src/component/header/HeaderDropdown.tsx b/frontend/src/component/header/HeaderDropdown.tsx new file mode 100644 index 00000000..d2dfd760 --- /dev/null +++ b/frontend/src/component/header/HeaderDropdown.tsx @@ -0,0 +1,38 @@ +import { Dropdown } from '@/component/common/dropdown/Dropdown.tsx'; +import { MdMenu, MdLocationOn } from 'react-icons/md'; +import { DropdownItem } from '@/component/common/dropdown/DropdownItem.tsx'; +import classNames from 'classnames'; + +// interface IDropdownContainerProps {} + +// TDDO : props로 전달되면 해석되도록 로직 변경 (props:props: IDropdownContainerProps) +export const HeaderDropdown = () => { + return ( +
    + + + + + + {[ + '사용자 1 보기', + '사용자 2 보기', + '사용자 3 보기', + '사용자 4 보기', + '사용자 5 보기', + ].map((e, i) => { + return ( + + {e} + + {/* 아이콘 색 변경 로직 찾기, 현재는 아이콘색이 반영이 안됨 수정할 사 */} + + ); + })} + + +
    + ); +}; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 3177f1a3..b1e5cba1 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -2,7 +2,7 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; import './index.css'; -import { App } from './App'; +import { App } from './App2'; // 우선은 폰트 다 포함시켰는데, 나중에 사용할 것들만 따로 뺴자. import '@fontsource/pretendard/100.css'; import '@fontsource/pretendard/200.css'; diff --git a/frontend/src/stories/DropdownButton.stories.tsx b/frontend/src/stories/DropdownButton.stories.tsx deleted file mode 100644 index 46745bee..00000000 --- a/frontend/src/stories/DropdownButton.stories.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import { fn } from '@storybook/test'; - -import { DropdownButton } from '@/component/common/dropdown/DropdownButton.tsx'; - -import { MdDensityMedium } from 'react-icons/md'; - -// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export -const meta = { - title: 'Dropdown/Button', - component: DropdownButton, - parameters: { - // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout - layout: 'centered', - }, - // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs - tags: ['autodocs'], - // More on argTypes: https://storybook.js.org/docs/api/argtypes - argTypes: { - children: { - control: 'object', - description: '자식 컴포넌트로 항상 리액트 노드를 넘겨준다.', - table: { - type: { summary: 'ReactNode' }, - }, - required: true, // 설명 목적으로 required 여부는 table 필드로 작성함 - }, - className: { - control: 'text', - description: '테일 윈드 기반의 클래스 이름을 넘겨준다.', - }, - }, - // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args - args: { onClick: fn() }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args -export const Default: Story = { - args: { - children: , - className: '', - }, -}; diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 95613736..1ee4edd1 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -47,6 +47,16 @@ module.exports = { float: '0 4px 20px rgba(0, 0, 0, 0.13)', basic: 'inset 0 0 3px rgba(0, 0, 0, 0.11)', }, + keyframes: { + smoothAppear: { + '0%': { opacity: '0', transform: 'translateY(-5%)' }, + '100%': { opacity: '1', transform: 'translateY(0)' }, + }, + }, + animation: { + smoothAppear: 'smoothAppear 0.5s ease-out', // 0.5s는 원하는 지속 시간 + smoothDisappear: 'smoothAppear 0.5s reverse ease-out', // 0.5s는 원하는 지속 시간 + }, }, // 필요한 커스터마이징을 여기서 설정 가능 }, plugins: [],