diff --git a/src/app/preview/dropdown/page.tsx b/src/app/preview/dropdown/page.tsx index fc9c2ef..9f84d94 100644 --- a/src/app/preview/dropdown/page.tsx +++ b/src/app/preview/dropdown/page.tsx @@ -1,66 +1,151 @@ 'use client'; +import Profile from '@/assets/icon/profile.svg'; import Dropdown from '@/components/common/Dropdown'; import { useState } from 'react'; +/** + * 드롭다운 컴포넌트 사용 예시 페이지 + * + * @example 기본 사용법 + * const options = [ + * { value: 'option1', label: '옵션 1' }, + * { value: 'option2', label: '옵션 2', onSelect: () => console.log('옵션 2 선택됨') } + * ]; + * + * + * + * @description 드롭다운 옵션 설명 + * 1. variant (드롭다운 스타일 변형) + * - 'default': 기본 스타일 (아이콘 없음) + * - 'icon': 회전하는 화살표 아이콘 + * - 'doubleArrow': 양방향 화살표 아이콘 + * - 'image': 이미지 전용 드롭다운 + * + * 2. size (드롭다운 크기) + * - 'l': Large (width: 122px) + * - 's': Small (width: 106px) + * + * 3. 스타일 커스터마이징 + * - className: 트리거 버튼 스타일 + * - contentClassName: 드롭다운 메뉴 스타일 + * + * 4. 상태 관리 + * - defaultValue: 초기 선택값 + * - onChange: 선택 변경 시 호출되는 함수 + * + * 5. 이미지 드롭다운 + * - imageProps: Image 컴포넌트 props (src, width, height, alt 등) + */ export default function Home() { - const [selectedFilter] = useState(null); + // 필터 옵션 예시: 정렬 기준 선택 + const [selectedFilter, setSelectedFilter] = useState(null); + + // 옵션 예시 1: 단순 선택 옵션 const filterOptions = [ - { value: 'all', label: '전체' }, - { value: 'completed', label: '완료됨' }, - { value: 'pending', label: '진행 중' }, + { value: 'latest', label: '최신순' }, + { value: 'oldest', label: '오래된순' }, + { value: 'like', label: '좋아요순' }, + ]; + + const filterAreaOptions = [ + { value: 'frontend', label: '프론트엔드' }, + { value: 'backend', label: '백엔드' }, + { value: 'designer', label: '디자이너' }, ]; - const menus = [ + + // 옵션 예시 2: 액션이 포함된 메뉴 + const menuOptions = [ { + value: 'mypage', label: '마이페이지', onSelect: () => { alert('마이페이지'); }, }, { + value: 'logout', label: '로그아웃', onSelect: () => { alert('로그아웃'); }, }, ]; - const widthSizeTemp = [ - { label: '전체전체전체전체전체' }, - { label: '완료됨' }, - { label: '진행 중' }, - ]; - return ( -
-

옵션

- - -
- - + return ( +
+ {/* 1. 기본 드롭다운 예시 */} +
+

1. 기본 드롭다운

+
+ {/* Large 사이즈 예시 */} + -
+ {/* Small 사이즈 예시 */} + +
+
- f.value === selectedFilter)?.label - : '필터 선택' - } - items={filterOptions} - sideOffset={8} - /> + {/* 2. 상태 관리가 포함된 드롭다운 예시 */} +
+

+ 2. 선택 상태가 있는 드롭다운 +

+ {/* 회전 화살표 아이콘 예시 */} + +
+ {/* 양방향 화살표 아이콘 예시 */} + +
-
+ {/* 3. 이미지 드롭다운 예시 */} +
+

3. 이미지 드롭다운

+ , + }} + /> +
- 트리거
} - items={widthSizeTemp} - /> + {/* 4. 커스텀 스타일링 예시 */} +
+

4. 커스텀 스타일 드롭다운

+ +
); } diff --git a/src/components/common/Dropdown.tsx b/src/components/common/Dropdown.tsx index ffd8488..8aed19a 100644 --- a/src/components/common/Dropdown.tsx +++ b/src/components/common/Dropdown.tsx @@ -1,93 +1,161 @@ -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '../ui/Dropdown'; +'use client'; -interface IDropdownProps { - /** - * 트리거 요소입니다. 버튼 또는 문자열 등 컴포넌트로 전달할 수 있습니다. - * @example - * 메뉴 열기} items={[]} /> - */ - trigger: React.ReactNode; - - /** - * 드롭다운에서 보여질 아이템 리스트입니다. - * - `label`: 필수값으로 화면에 보여지는 텍스트입니다. - * - `value`: 아이템의 식별을 위한 값 (선택 사항). - * - `onSelect`: 아이템 클릭 시 실행될 함수 (선택 사항). - * @example - * console.log('마이페이지 이동') }, - * { label: '로그아웃', value: 'logout', onSelect: () => console.log('로그아웃') }, - * ]} - * /> - */ - items: { label: string; value?: string; onSelect?: () => void }[]; +import { cn } from '@/util/cn'; +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; +import { ChevronDown, ChevronsUpDown } from 'lucide-react'; +import Image from 'next/image'; +import * as React from 'react'; - /** - * 아이템의 크기입니다. `'s'`(작은 크기)와 `'l'`(큰 크기) 두 가지 옵션이 있습니다. - * @default "l" - * @example - * - */ - size?: 's' | 'l'; - - /** - * 트리거 요소와의 간격입니다. `px` 단위로 설정됩니다. - * @default 4 - * @example - * - */ - sideOffset?: number; +interface IDropdownOption { + value: string; + label: string; + onSelect?: () => void; +} - /** - * 스타일을 커스텀할 수 있도록 제공됩니다. Tailwind CSS 또는 추가 스타일을 적용할 수 있습니다. - * @example - * - */ - className?: string; +interface IDropdownProps { + options?: IDropdownOption[]; // 드롭다운에 표시될 옵션들 + onChange?: (value: string) => void; // 선택 변경 시 호출될 콜백 + variant?: 'default' | 'icon' | 'doubleArrow' | 'image'; // 드롭다운 스타일 변형 + size?: 's' | 'l'; // 크기 옵션 + className?: string; // 트리거 스타일 커스터마이징 + contentClassName?: string; // 드롭다운 메뉴 스타일 커스터마이징 + imageProps?: + | { + component: React.ReactNode; + } + | (Omit, 'alt'> & { + alt?: string; + }); // 이미지 드롭다운을 위한 속성 + sideOffset?: number; // 드롭다운 메뉴와 트리거 사이 간격 + trigger?: string; // 선택되지 않았을 때 표시될 텍스트 } -/** - * 드롭다운 컴포넌트 - * @example - * 메뉴} - * items={[ - * { label: '마이페이지', value: 'mypage', onSelect: () => console.log('마이페이지 이동') }, - * { label: '로그아웃', value: 'logout', onSelect: () => console.log('로그아웃') }, - * ]} - * size="s" - * sideOffset={8} - * className="custom-dropdown" - * /> - */ const Dropdown = ({ - trigger, - items, - size, - sideOffset, + options = [], + onChange, + variant = 'default', + size = 'l', + sideOffset = 4, className, + contentClassName, + imageProps, + trigger = '선택하세요', }: IDropdownProps) => { + const [isOpen, setIsOpen] = React.useState(false); // 드롭다운 열림/닫힘 상태 + const [selectedValue, setSelectedValue] = React.useState(null); // 현재 선택된 값 + + const selectedOption = options.find((opt) => opt.value === selectedValue); // 선택된 옵션 찾기 + + const sizeClasses = { + s: 'h-10 w-[106px]', + l: 'h-10 w-[122px]', + }[size]; + + const handleSelect = (option: IDropdownOption) => { + setSelectedValue(option.value); // 내부 상태 업데이트 + onChange?.(option.value); // 외부 콜백 호출 + option.onSelect?.(); // 옵션별 콜백 호출 + }; + + const renderTriggerContent = () => { + if (imageProps) { + if ('component' in imageProps) { + return ( +
+ {imageProps.component} +
+ ); + } + return ( +
+ {imageProps.alt +
+ ); + } //이미지 드롭다운 + + if (variant === 'default') { + return {trigger}; + } // 기본 스타일 + + // 아이콘이 있는 variant의 경우 선택된 값이 있으면 그 값을, 없으면 trigger를 표시 + return ( + <> + {selectedOption ? selectedOption.label : trigger} + {variant === 'doubleArrow' ? ( + + ) : ( + + )} + + ); // 아이콘이 있는 스타일 + }; + + const shouldChangeBackground = + variant !== 'default' && + variant !== 'image' && + !className?.includes('w-[460px]'); + return ( - - {trigger} - - {items.map((item) => ( - - {item.label} - - ))} - - + + + {renderTriggerContent()} + + + + + {options.map((option) => ( + handleSelect(option)} + className={cn( + 'relative flex w-full cursor-pointer select-none items-center', + 'typo-body1 h-[34px] rounded-[10px] px-[12px] py-[8px]', + 'text-Cgray400 hover:bg-Cgray300 hover:text-Cgray700', + 'outline-none transition-colors', + size === 's' ? 'typo-body2' : 'typo-body1', + option.value === selectedValue && 'bg-Cgray300 text-Cgray700', + )} + > + {option.label} + + ))} + + + ); }; + export default Dropdown; diff --git a/src/components/common/Header.tsx b/src/components/common/Header.tsx index abe9ec7..6418643 100644 --- a/src/components/common/Header.tsx +++ b/src/components/common/Header.tsx @@ -25,14 +25,34 @@ const BeforeLogin = () => { const AfterLogin = () => { const router = useRouter(); const menu = [ - { label: '내 모임', onSelect: () => router.push('/my-meeting') }, - { label: '마이페이지', onSelect: () => router.push('/my-page') }, - { label: '로그아웃', onSelect: () => console.log('로그아웃') }, + { + value: 'mymeeting', + label: '내 모임', + onSelect: () => router.push('/my-meeting'), + }, + { + value: 'mypage', + label: '마이페이지', + onSelect: () => router.push('/my-page'), + }, + { + value: 'logout', + label: '로그아웃', + onSelect: () => console.log('로그아웃'), + }, ]; return (