Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions components/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { useEffect, useRef, useState } from 'react';

import Menu from './Menu';

interface DropdownProps {
options: string[];
dropdownSize?: string;
onSelect: (option: string) => void;
}

/**
* 드롭다운 컴포넌트
* @param options 메뉴팝업에 들어가는 옵션을 담을 배열
* @param dropdownSize 팝업의 크기
* @param onSelect 선택한 옵션을 부모 컴포넌트로 반환
*/

export default function Dropdown({
options,
onSelect,
dropdownSize = 'w-auto',
}: DropdownProps) {
const [selectedOption, setSelectedOption] = useState(options[0]); //현재 선택된 옵션
const [isOpen, setIsOpen] = useState(false); //드롭다운 열기/닫기
const dropdownRef = useRef<HTMLDivElement>(null); // 드롭다운 DOM 요소 참조

//선택한 옵션을 상태로 설정, 부모 컴포넌트로 전달
const handleOptionClick = (option: string) => {
setSelectedOption(option);
setIsOpen(false);
onSelect(option);
};

//외부 클릭시 드롭다운 닫기
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};

document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);

return (
<div ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className={`${dropdownSize} flex items-center justify-between rounded-xl border border-gray-300 bg-background px-5 py-3.5 text-14 leading-none text-gray-400 hover:border-green-200 focus:ring-1 focus:ring-green-200`}
>
{selectedOption}
<img
src="/icon/icon-arrowdown.svg"
className={`h-4 w-4 ${isOpen ? 'rotate-180' : 'rotate-0'}`}
/>
</button>

{isOpen && (
<Menu
options={options}
onSelect={handleOptionClick}
menuSize={dropdownSize}
/>
)}
</div>
);
}
30 changes: 30 additions & 0 deletions components/Menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
interface MenuProps {
options: string[];
menuSize?: string;
onSelect: (option: string) => void;
}

/**
* 메뉴팝업을 띄우는 컴포넌트
* @param options 메뉴팝업에 들어가는 옵션을 담을 배열
* @param menuSize 팝업의 크기
* @param onSelect 선택한 옵션을 반환
*/

export default function Menu({ options, onSelect, menuSize }: MenuProps) {
return (
<ul
className={`${menuSize} absolute z-10 mt-2 rounded-xl border border-gray-300 bg-background p-1 text-14 shadow-custom`}
>
{options.map((option, index) => (
<li
key={index}
onClick={() => onSelect(option)}
className={`w-auto cursor-pointer rounded-md px-3 py-2.5 hover:bg-green-100`}
>
{option}
</li>
))}
</ul>
);
}
12 changes: 10 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,16 @@
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^8.57.1",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2",
"@typescript-eslint/eslint-plugin": "^8.18.0",
"@typescript-eslint/parser": "^8.18.0",
"autoprefixer": "^10.4.20",
"eslint": "^9.16.0",
"eslint-config-next": "^15.1.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-next": "^0.0.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-simple-import-sort": "^12.1.1",
Expand Down
37 changes: 37 additions & 0 deletions pages/test/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState } from 'react';

import Button from '@/components/Button';
import Dropdown from '@/components/Dropdown';
import LinkBar from '@/components/LinkBar';
import SnackBar from '@/components/SnackBar';

Expand All @@ -24,6 +25,32 @@ export default function Test() {
};
// ----snackBar(end)----

//-----dropdown(start)-----
const options = ['옵션1', '옵션2', '옵션3'];

const handleOption1 = () => {
console.log('옵션1');
};

const handleOption2 = () => {
console.log('옵션2');
};

const handleOption3 = () => {
console.log('옵션3');
};

const handleOptionSelect = (option: string) => {
if (option === '옵션1') {
handleOption1();
} else if (option === '옵션2') {
handleOption2();
} else if (option === '옵션3') {
handleOption3();
}
};
//-----dropdown(end)-----

return (
<div className="px-4 py-10">
<table className="w-full border-collapse border border-gray-300">
Expand Down Expand Up @@ -67,6 +94,16 @@ export default function Test() {
<LinkBar link="https://www.google.com" />
</td>
</tr>
<tr className="border-b border-gray-300">
<td className={commonCellClass}>Dropdown</td>
<td className={commonRowClass}>
<Dropdown
options={options}
onSelect={handleOptionSelect}
dropdownSize="w-40"
/>
</td>
</tr>
</tbody>
</table>
</div>
Expand Down
3 changes: 3 additions & 0 deletions public/icon/icon-arrowdown.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading