diff --git a/packages/web/src/app/favicon.ico b/packages/web/src/app/favicon.ico index 718d6fe..d4e29cd 100644 Binary files a/packages/web/src/app/favicon.ico and b/packages/web/src/app/favicon.ico differ diff --git a/packages/web/src/common/components/Forms/Select.tsx b/packages/web/src/common/components/Select/index.tsx similarity index 67% rename from packages/web/src/common/components/Forms/Select.tsx rename to packages/web/src/common/components/Select/index.tsx index b146aff..9c14102 100644 --- a/packages/web/src/common/components/Forms/Select.tsx +++ b/packages/web/src/common/components/Select/index.tsx @@ -1,8 +1,9 @@ import React, { useState, useRef, useEffect } from "react"; import styled, { css } from "styled-components"; import { KeyboardArrowDown, KeyboardArrowUp } from "@mui/icons-material"; -import Label from "./_atomic/Label"; -import ErrorMessage from "./_atomic/ErrorMessage"; +import Label from "../Forms/_atomic/Label"; +import ErrorMessage from "../Forms/_atomic/ErrorMessage"; +import Dropdown from "./Dropdown"; export interface SelectItem { label: string; @@ -15,15 +16,15 @@ interface SelectProps { label?: string; errorMessage?: string; disabled?: boolean; - selectedValue?: string; - onSelect?: (value: string) => void; + selectedValue?: string | string[]; + multi?: boolean; // 다중 선택 여부 추가 + onSelect?: (value: string | string[]) => void; setErrorStatus?: (hasError: boolean) => void; } const DropdownContainer = styled.div` gap: 4px; position: relative; - display: flex; `; const disabledStyle = css` @@ -38,7 +39,7 @@ const StyledSelect = styled.div<{ isOpen?: boolean; }>` width: 100%; - padding: 8px 32px 8px 12px; + padding: 8px 12px; outline: none; cursor: pointer; background-color: ${({ theme }) => theme.colors.WHITE}; @@ -62,21 +63,7 @@ const StyledSelect = styled.div<{ ${({ disabled }) => disabled && disabledStyle} `; -const Dropdown = styled.div` - position: absolute; - display: flex; - flex-direction: column; - width: 100%; - margin-top: 40px; - padding: 8px; - border: 1px solid ${({ theme }) => theme.colors.GRAY[100]}; - border-radius: 4px; - background-color: ${({ theme }) => theme.colors.WHITE}; - gap: 4px; - z-index: 1000; // Ensure the dropdown appears above other content -`; - -const Option = styled.div<{ selectable?: boolean }>` +const Option = styled.div<{ selectable?: boolean; isSelected?: boolean }>` gap: 10px; border-radius: 4px; padding: 4px 12px; @@ -86,6 +73,8 @@ const Option = styled.div<{ selectable?: boolean }>` font-weight: ${({ theme }) => theme.fonts.WEIGHT.REGULAR}; color: ${({ theme, selectable }) => selectable ? theme.colors.BLACK : theme.colors.GRAY[100]}; + background-color: ${({ theme, isSelected }) => + isSelected ? theme.colors.GREEN[100] : theme.colors.WHITE}; ${({ selectable }) => selectable && css` @@ -137,6 +126,7 @@ const Select: React.FC = ({ label = "", disabled = false, selectedValue = "", + multi = false, // 기본값을 false로 설정 onSelect = () => {}, setErrorStatus = () => {}, }) => { @@ -145,8 +135,11 @@ const Select: React.FC = ({ const containerRef = useRef(null); useEffect(() => { - setErrorStatus(!!errorMessage || (!selectedValue && items.length > 0)); - }, [errorMessage, selectedValue, items.length, setErrorStatus]); + const hasError = + (multi && Array.isArray(selectedValue) && selectedValue.length === 0) || + (!multi && !selectedValue && items.length > 0); + setErrorStatus(!!errorMessage || hasError); + }, [errorMessage, selectedValue, items.length, multi, setErrorStatus]); useEffect(() => { function handleClickOutside(event: MouseEvent) { @@ -176,14 +169,40 @@ const Select: React.FC = ({ const handleOptionClick = (item: SelectItem) => { if (item.selectable) { - onSelect(item.value); + if (multi) { + let newSelectedValue = Array.isArray(selectedValue) + ? [...selectedValue] + : []; + if (newSelectedValue.includes(item.value)) { + newSelectedValue = newSelectedValue.filter(val => val !== item.value); + } else { + newSelectedValue.push(item.value); + } + onSelect(newSelectedValue); + } else { + onSelect(item.value); + } + } + if (!multi) { setIsOpen(false); } }; - const selectedLabel = - items.find(item => item.value === selectedValue)?.label || - "항목을 선택해주세요"; + let selectedLabel: string; + + if (multi) { + if (Array.isArray(selectedValue) && selectedValue.length > 0) { + selectedLabel = items + .filter(item => selectedValue.includes(item.value)) + .map(item => item.label) + .join(", "); + } else { + selectedLabel = "항목을 선택해주세요"; + } + } else { + const foundItem = items.find(item => item.value === selectedValue); + selectedLabel = foundItem ? foundItem.label : "항목을 선택해주세요"; + } return ( @@ -192,13 +211,19 @@ const Select: React.FC = ({ 0 && !isOpen + hasOpenedOnce && + multi && + Array.isArray(selectedValue) && + selectedValue.length === 0 && + !isOpen } disabled={disabled} onClick={handleSelectClick} isOpen={isOpen} > - + 0 : !!selectedValue} + > {selectedLabel} @@ -216,6 +241,12 @@ const Select: React.FC = ({ - {hasOpenedOnce && !selectedValue && items.length > 0 && ( - - {errorMessage || "필수로 선택해야 하는 항목입니다"} - - )} + {hasOpenedOnce && + multi && + Array.isArray(selectedValue) && + selectedValue.length === 0 && ( + + {errorMessage || "필수로 선택해야 하는 항목입니다"} + + )} ); diff --git a/packages/web/stories/Forms/Select.stories.tsx b/packages/web/stories/Forms/Select.stories.tsx index c649d36..6a9353b 100644 --- a/packages/web/stories/Forms/Select.stories.tsx +++ b/packages/web/stories/Forms/Select.stories.tsx @@ -1,8 +1,9 @@ import type { Meta, StoryObj } from "@storybook/react"; import React from "react"; -import Select from "@sparcs-students/web/common/components/Forms/Select"; + import { useArgs } from "@storybook/client-api"; import { fn } from "@storybook/test"; +import Select from "@sparcs-students/web/common/components/Select"; const meta: Meta = { title: "components/Forms/Select", diff --git a/packages/web/tsconfig.json b/packages/web/tsconfig.json index 06e8a23..9d5a843 100644 --- a/packages/web/tsconfig.json +++ b/packages/web/tsconfig.json @@ -23,4 +23,4 @@ }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] -} +} \ No newline at end of file