diff --git a/src/components/Button.tsx b/src/components/Button.tsx index afe5c84..b0ac5f5 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -1,7 +1,6 @@ import React from "react"; -import clsx from "clsx"; -import { twMerge } from "tailwind-merge"; +import { cn } from "@/utils/cn"; type ButtonVariant = "primary" | "white"; type ButtonTextSize = "lg" | "md" | "sm"; @@ -38,14 +37,12 @@ export default function Button({ }: ButtonProps) { const baseClasses = "rounded-md"; - const mergedClasses = twMerge( - clsx( - baseClasses, - textSizeClassMap[textSize], - disabled ? disabledClass : variantClassMap[variant], - fullWidth && "w-full", - className, - ), + const mergedClasses = cn( + baseClasses, + textSizeClassMap[textSize], + disabled ? disabledClass : variantClassMap[variant], + fullWidth && "w-full", + className, ); return ( diff --git a/src/components/Select.tsx b/src/components/Select.tsx new file mode 100644 index 0000000..07dfe44 --- /dev/null +++ b/src/components/Select.tsx @@ -0,0 +1,193 @@ +import { + useState, + useEffect, + useRef, + ReactNode, + SelectHTMLAttributes, + useMemo, +} from "react"; + +import { DropdownDown, DropdownUp } from "@/assets/icon"; +import { cn } from "@/utils/cn"; + +const sizeMap = { + lg: "py-4 px-5 text-[1rem]", + sm: "p-2.5 text-sm", +} as const; + +interface Option { + label: string; + value: string; +} + +interface SelectProps + extends Omit< + SelectHTMLAttributes, + "size" | "onChange" | "disabled" + > { + id?: string; + label?: string; + options: Option[]; + value?: string; + onValueChange?: (value: string) => void; + placeholder?: string; + size?: keyof typeof sizeMap; + fullWidth?: boolean; + className?: string; + wrapperClassName?: string; +} + +// 라벨과 입력 영역을 감싸는 공통 컴포넌트 +function Field({ + id, + label, + children, +}: { + id?: string; + label?: string; + children: ReactNode; +}) { + return ( +
+ {label && ( + + )} + {children} +
+ ); +} + +function Select({ + id, + label, + options, + value, + onValueChange, + placeholder = "선택", + size = "lg", + fullWidth, + className, + wrapperClassName, + ...rest +}: SelectProps) { + const [open, setOpen] = useState(false); + const [buttonWidth, setButtonWidth] = useState(0); + + const wrapperRef = useRef(null); + const buttonRef = useRef(null); + + const selectedOption = useMemo(() => { + return options.find((option) => option.value === value); + }, [options, value]); + + const wrapperClassNames = cn( + "relative", + { + "w-full": fullWidth, + }, + wrapperClassName, + ); + + const buttonClassNames = cn( + "flex items-center justify-between rounded-[0.375rem] cursor-pointer", + { + "w-full": fullWidth, + "bg-white border border-gray-30": size === "lg", + "bg-gray-10 font-bold": size === "sm", + }, + value ? "text-black" : "text-gray-40", + sizeMap[size], + className, + ); + + const listClassNames = cn( + "absolute top-full left-0 mt-1 border rounded-[0.375rem] bg-white border-gray-30 text-black shadow-lg z-10 max-h-48 overflow-y-auto", + ); + + const handleSelect = (selectedValue: string) => { + onValueChange?.(selectedValue); + setOpen(false); + }; + + // 드롭다운 외부 클릭 시 닫기 + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Node; + if ( + !buttonRef.current?.contains(target) && + !wrapperRef.current?.contains(target) + ) { + setOpen(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + + // 버튼 너비 측정 (드롭다운 너비 일치시키기 위함) + useEffect(() => { + if (buttonRef.current) { + const rect = buttonRef.current.getBoundingClientRect(); + setButtonWidth(rect.width); + } + }, [open, fullWidth, size, value]); // 버튼 사이즈가 변할 수 있는 경우 + + return ( + +
+ + + {open && ( +
    + {options.map((option) => ( +
  • + +
  • + ))} +
+ )} +
+
+ ); +} + +export default Select; diff --git a/src/index.css b/src/index.css index 1da68bb..691f1af 100644 --- a/src/index.css +++ b/src/index.css @@ -2,3 +2,27 @@ @import "tailwindcss"; @import "./styles/base.css"; @import "./styles/utilities.css"; + +::-webkit-scrollbar { + width: 12px; +} + +::-webkit-scrollbar-thumb { + background-color: #7d7986; + border: 4px solid transparent; + background-clip: padding-box; + border-radius: 9999px; + min-height: 60px; +} + +::-webkit-scrollbar-thumb:hover { + background-color: #636169; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-button { + display: none; +}