Next.js 프로젝트가 설정되어 있는지 확인하세요. 그렇지 않은 경우 하나를 만듭니다.
npx 생성-다음-앱 my-app --typescriptcd my-app
필수 shadcn 구성요소를 설치합니다.
npx shadcn@최신 초기화 npx shadcn@latest 추가 명령 팝오버 버튼 구분 기호 배지
components
디렉터리에 multi-select.tsx
만듭니다.
// src/comComponents/multi-select.tsximport * as React from "react";import { cva, type VariantProps } from "class-variance-authority";import { 체크아이콘, X서클, 쉐브론다운, X아이콘, WandSparkles,} "lucide-react"에서; "@/lib/utils"에서 가져오기 { cn }; "@/comComponents/ui/separator"에서 가져오기 { 구분 기호 }; "@/comComponents/ui/에서 가져오기 { 버튼 } 버튼";import { 배지 } "@/comComponents/ui/badge"에서;import { 팝오버, 팝오버컨텐츠, PopoverTrigger,} "@/comComponents/ui/popover";import { 명령, 명령비어 있음, 명령그룹, 명령입력, 명령항목, 명령 목록, CommandSeparator,} from "@/comComponents/ui/command";/** * 다양한 스타일을 처리하기 위한 다중 선택 구성 요소의 변형입니다. * "variant" 소품을 기반으로 다양한 스타일을 정의하기 위해 class-variance-authority(cva)를 사용합니다. */const multiSelectVariants = cva( "m-1 전환 완화 지연-150 hover:-translate-y-1 hover:scale-110 기간-300", {변형: { 변형: {기본값: "테두리-전경/10 텍스트-전경 bg-카드 hover:bg-카드/80",보조: "테두리-전경/10 bg-보조 텍스트-보조-전경 hover:bg- 보조/80",파괴: "테두리 투명 bg-파괴 텍스트 파괴 전경 hover:bg-파괴/80",반전: "반전됨", },},defaultVariants: { 변형: "default",}, });/** * MultiSelect 구성 요소에 대한 속성 */interface MultiSelectProps 확장 React.ButtonHTMLAttributes<HTMLButtonElement>,VariantProps<typeof multiSelectVariants> { /** * 다중 선택 컴포넌트에 표시될 옵션 개체의 배열입니다. * 각 옵션 개체에는 레이블, 값 및 선택적 아이콘이 있습니다. */ options: {/** 옵션에 대해 표시할 텍스트입니다. */label: string;/** 옵션과 관련된 고유 값입니다. */value: string;/** 옵션과 함께 표시할 선택적 아이콘 구성 요소입니다. */icon?: React.ComponentType<{ className?: 문자열 }>; }[]; /** * 선택한 값이 변경되면 콜백 함수가 트리거됩니다. * 새로 선택한 값의 배열을 받습니다. */ onValueChange: (값: 문자열[]) => void; /** 구성 요소가 마운트될 때 기본적으로 선택되는 값입니다. */ defaultValue?: 문자열[]; /** * 선택된 값이 없을 때 표시될 자리 표시자 텍스트입니다. * 선택사항, 기본값은 "옵션 선택"입니다. */ 자리 표시자?: 문자열; /** * 시각 효과(예: 뱃지 튕기기)의 애니메이션 지속 시간(초)입니다. * 선택사항, 기본값은 0(애니메이션 없음)입니다. */ 애니메이션?: 숫자; /** * 표시할 최대 항목 수입니다. 추가로 선택한 항목이 요약됩니다. * 선택사항, 기본값은 3입니다. */ maxCount?: 숫자; /** * 팝오버의 양식입니다. true로 설정하면 외부 요소*와의 상호 작용이 비활성화되고 팝오버 콘텐츠만 스크린 리더에 표시됩니다. * 선택사항, 기본값은 false입니다. */ modalPopover?: 부울; /** * true인 경우 다중 선택 구성 요소를 다른 구성 요소의 하위로 렌더링합니다. * 선택사항, 기본값은 false입니다. */ asChild?: 부울; /** * 다중 선택 구성 요소에 사용자 정의 스타일을 적용하기 위한 추가 클래스 이름입니다. * 선택 사항으로 사용자 정의 스타일을 추가하는 데 사용할 수 있습니다. */ className?: 문자열;}export const MultiSelect = React.forwardRef< HTML버튼요소, MultiSelectProps>( ({ options, onValueChange, 변형, defaultValue = [], 자리 표시자 = "옵션 선택", 애니메이션 = 0, maxCount = 3, modalPopover = false, asChild = false, className, ...props},ref ) => {const [selectedValues, setSelectedValues] = React.useState<string[]>(defaultValue);const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);const [isAnimating, setIsAnimating] = React.useState(false) ;const handlerInputKeyDown = ( 이벤트: React.KeyboardEvent<HTMLInputElement>) => { if (event.key === "입력") {setIsPopoverOpen(true); } else if (event.key === "Backspace" && !event.currentTarget.value) {const newSelectedValues = [...selectedValues];newSelectedValues.pop();setSelectedValues(newSelectedValues);onValueChange(newSelectedValues); }};const 토글옵션 = (옵션: 문자열) => { const newSelectedValues = selectedValues.includes(옵션)? selectedValues.filter((value) => value !== option): [...selectedValues, option]; setSelectedValues(newSelectedValues); onValueChange(newSelectedValues);};const handlerClear = () => { setSelectedValues([]); onValueChange([]);};const handlerTogglePopover = () => { setIsPopoverOpen((prev) => !prev);};constclearExtraOptions = () => { const newSelectedValues = selectedValues.slice(0, maxCount); setSelectedValues(newSelectedValues); onValueChange(newSelectedValues);};constggleAll = () => { if (selectedValues.length === options.length) {handleClear(); } else {const allValues = options.map((option) => option.value);setSelectedValues(allValues);onValueChange(allValues); }};return ( <Popoveropen={isPopoverOpen}onOpenChange={setIsPopoverOpen}modal={modalPopover} ><PopoverTrigger asChild> <Buttonref={ref}{...props}onClick={handleTogglePopover}className={cn( "flex w-전체 p-1 둥근 MD 테두리 min-h-10 h-자동 항목 중심 justify-between bg-inherit hover:bg-inherit", className)} >{selectedValues.length > 0 ? ( <div className="flex justify-between items-center w-full"><div className="flex flex- 항목 포장 센터"> {selectedValues.slice(0, maxCount).map((value) => {const option = options.find((o) => o.value === value);const IconComponent = option?.icon;return ( <Badgekey={value}className={cn( isAnimating ? "animate-bounce" : "", multiSelectVariants({ 변형 }))}style={{ animationDuration: `${animation}s` }} >{IconComponent && ( <IconComponent className="h-4 w-4 mr-2" />)}{option?.label}<XCircle className="ml-2 h-4 w-4 커서-포인터" onClick={(event) => {event.stopPropagation();toggleOption(value) }} /> </Badge>) })} {selectedValues.length > maxCount && (<Badge className={cn("bg-transparent text-foreground) border-foreground/1 hover:bg-transparent",isAnimating ? "animate-bounce": "",multiSelectVariants({ 변형 }) )} style={{ animationDuration: `${animation}s` }}> {`+ ${selectedValues.length - maxCount} more`} <XCircleclassName= "ml-2 h-4 w-4 커서 포인터"onClick={(event) => { event.stopPropagation(); clearExtraOptions();}} /></Badge> )}</div><div className="flex items-center justify-between"> <XIconclassName="h-4 mx-2 커서 포인터 텍스트 음소거-전경 "onClick={(event) => { event.stopPropagation(); handlerClear();}} /> <Separatororientation="vertical"className="flex min-h-6 h-full" /> <ChevronDown className="h-4 mx-2 커서 포인터 텍스트-음소거-전경" / ></div> </div>) : ( <div className="flex items-center justify-between w-full mx-auto"><span className="text-sm text-muted-foreground mx-3"> {placeholder}</span><ChevronDown className="h-4 커서-포인터 text-muted-foreground mx-2" /> </div>)} </Button>< /PopoverTrigger><PopoverContent className="w-auto p-0" align="start" onEscapeKeyDown={() => setIsPopoverOpen(false)}> <Command><CommandInput placeholder="Search..." onKeyDown={handleInputKeyDown}/><CommandList> <CommandEmpty>결과가 없습니다.</CommandEmpty> <CommandGroup><CommandItem key="all" onSelect={toggleAll} className ="cursor-pointer"> <divclassName={cn( "mr-2 플렉스 h-4 w-4 항목 중심 정렬 중심 둥근 SM 테두리 테두리 기본", selectedValues.length === options.length? "bg-기본 텍스트-기본-전경": "불투명도-50 [&_svg]:invisible")} >< CheckIcon className="h-4 w-4" /> </div> <span>(모두 선택)</span></CommandItem>{options.map((option) => { const isSelected = selectedValues.includes(option.value); return (<CommandItem key={option.value} onSelect={() =>ggleOption(option.value)} className="cursor-pointer"> <divclassName={cn( "mr-2 flex h-4 w-4 항목-중심 정렬-중심 둥근-sm 테두리 테두리-기본", isSelected? "bg-기본 텍스트-기본-전경": "불투명도-50 [&_svg]:invisible")} ><CheckIcon className="h-4 w-4" /> </div> {option.icon && (<option.icon className="mr-2 h-4 w-4 text -muted-foreground" /> )} <span>{option.label}</span></CommandItem> );})} </CommandGroup> <CommandSeparator /> <CommandGroup><div className="flex items-center justify-between"> {selectedValues.length > 0 && (<> <CommandItemonSelect={handleClear}className="flex-1 justify-centercursor-pointer" >Clear </ CommandItem> <Separatororientation="vertical"className="flex min-h-6 h-full" /></> )} <CommandItemonSelect={() => setIsPopoverOpen(false)}className="flex-1 justify-centercursor-pointer max-w-full" >닫기 </CommandItem></div> </CommandGroup></CommandList> < /Command></PopoverContent>{animation > 0 && selectedValues.length > 0 && ( <WandSparklesclassName={cn( "커서 포인터 my-2 텍스트 전경 bg-배경 w-3 h-3", isAnimating ? "" : "text-muted-foreground")}onClick={() => setIsAnimating(!isAnimating)} />)} </Popover>); });MultiSelect.displayName =</sp