Repository: codeit-sprint14-part3-4/Taskify
Pull request #24: [Feature/dropdown] StatusDropdown과 UserDropdown 컴포넌트 UI 구현
Author: login: Parkchanyoung0710
Labels: name: ✨기능 추가 (Feature)
description: 새로운 기능을 추가하는 작업
Created: 2025-04-27T11:44:59Z | Merged: 2025-04-28T06:32:52Z

## 📌 PR 개요
StatusDropdown과 UserDropdown 컴포넌트를 추가하고, 드롭다운 기능을 구현했습니다.

## 🏃‍♂️‍➡️ 요구사항
- [x] ESC 키를 누르면 드롭다운이 닫힌다.
- [x] 화살표 회전: 드롭다운이 열리면 화살표가 위로 향하게 회전, 닫힐 경우 아래로 회전
- [x] 버튼 테두리 색상 변경: 드롭다운 버튼을 클릭하면 테두리가 보라색으로 바뀜. 드롭다운 목록 안에서 항목을 클릭해서 닫히면 다시 회색으로 변경된다.
- [x] 체크표시: 선택한 항목 옆에만 체크(✔️) 표시가 나타난다.
- [x] 스크롤바: 목록이 4개부터는 스크롤바가 나타난다.
- [x] 선택한 상태(Status)와 사용자(User)에 따라 UI가 반영된다.
- [x] mousedown으로 다른곳 클릭시 드롭다운 닫기
- [x] TAB으로 선택 가능
## 🔥 변경 사항
- [x] 새로운 기능 추가
- [ ] 버그 수정
- [x] 리팩토링
- [ ] 문서 수정

## 📝 변경 내용
- [x] StatusDropdown은 value: Status, onChange: (status: Status) => void props를 받아 상태(Status)를 선택할 수 있도록 구현
- [x] UserDropdown은 selectedUser: User, onChange: (user: User) => void props를 받아 유저를 선택할 수 있도록 구현

## 📷 스크린샷 (선택)
### 기본 UI
![닫힘 dropdown](https://github.com/user-attachments/assets/0e136b67-37f1-482f-b8cc-bb6a328a7198)
![기본 dropdown](https://github.com/user-attachments/assets/26b755bf-17b4-42e5-8684-d70b4c6dc69a)
### ✨리팩토링 후
![리팩토링 dropdown state](https://github.com/user-attachments/assets/660d451f-040c-4391-aca7-05447aace03e)
![담당자](https://github.com/user-attachments/assets/7d43283b-4c0e-4591-8b10-f4982526d83a)
뱃지 색상, 폰트 스타일, border-radius 8px로 통일, background 색상 통일, enum으로 타입 안전성 향상
## 🚧 관련 이슈

<!-- 관련 이슈 번호가 있다면 적어주세요 (e.g. #123 또는 ISSUE-123 과 같은 JIRA key) -->

## 💡 추가 정보
추후 API 연동 시 Status, User 리스트를 서버에서 받아올 수 있도록 수정 예정
<!-- 리뷰어가 알아야 할 추가 정보가 있다면 여기에 적어주세요 -->


Diff:
diff --git a/package-lock.json b/package-lock.json
index e16aab8..a519d9f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4915,6 +4915,126 @@
           "optional": true
         }
       }
+    },
+    "node_modules/@next/swc-darwin-arm64": {
+      "version": "14.2.28",
+      "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.28.tgz",
+      "integrity": "sha512-kzGChl9setxYWpk3H6fTZXXPFFjg7urptLq5o5ZgYezCrqlemKttwMT5iFyx/p1e/JeglTwDFRtb923gTJ3R1w==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-darwin-x64": {
+      "version": "14.2.28",
+      "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.28.tgz",
+      "integrity": "sha512-z6FXYHDJlFOzVEOiiJ/4NG8aLCeayZdcRSMjPDysW297Up6r22xw6Ea9AOwQqbNsth8JNgIK8EkWz2IDwaLQcw==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-arm64-gnu": {
+      "version": "14.2.28",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.28.tgz",
+      "integrity": "sha512-9ARHLEQXhAilNJ7rgQX8xs9aH3yJSj888ssSjJLeldiZKR4D7N08MfMqljk77fAwZsWwsrp8ohHsMvurvv9liQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-arm64-musl": {
+      "version": "14.2.28",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.28.tgz",
+      "integrity": "sha512-p6gvatI1nX41KCizEe6JkF0FS/cEEF0u23vKDpl+WhPe/fCTBeGkEBh7iW2cUM0rvquPVwPWdiUR6Ebr/kQWxQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-x64-gnu": {
+      "version": "14.2.28",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.28.tgz",
+      "integrity": "sha512-nsiSnz2wO6GwMAX2o0iucONlVL7dNgKUqt/mDTATGO2NY59EO/ZKnKEr80BJFhuA5UC1KZOMblJHWZoqIJddpA==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-x64-musl": {
+      "version": "14.2.28",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.28.tgz",
+      "integrity": "sha512-+IuGQKoI3abrXFqx7GtlvNOpeExUH1mTIqCrh1LGFf8DnlUcTmOOCApEnPJUSLrSbzOdsF2ho2KhnQoO0I1RDw==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-win32-arm64-msvc": {
+      "version": "14.2.28",
+      "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.28.tgz",
+      "integrity": "sha512-l61WZ3nevt4BAnGksUVFKy2uJP5DPz2E0Ma/Oklvo3sGj9sw3q7vBWONFRgz+ICiHpW5mV+mBrkB3XEubMrKaA==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-win32-ia32-msvc": {
+      "version": "14.2.28",
+      "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.28.tgz",
+      "integrity": "sha512-+Kcp1T3jHZnJ9v9VTJ/yf1t/xmtFAc/Sge4v7mVc1z+NYfYzisi8kJ9AsY8itbgq+WgEwMtOpiLLJsUy2qnXZw==",
+      "cpu": [
+        "ia32"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
     }
   }
 }
diff --git a/public/assets/image/arrow-down.svg b/public/assets/image/arrow-down.svg
new file mode 100644
index 0000000..ca8ba7a
--- /dev/null
+++ b/public/assets/image/arrow-down.svg
@@ -0,0 +1,3 @@
+<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12.4802 15.355L9.06643 11.9413C9.01935 11.8942 8.98226 11.8416 8.95518 11.7836C8.92809 11.7255 8.91455 11.6632 8.91455 11.5969C8.91455 11.4641 8.95944 11.3488 9.04921 11.2509C9.13896 11.153 9.25727 11.104 9.40414 11.104H16.5957C16.7426 11.104 16.8609 11.1534 16.9507 11.2523C17.0404 11.3512 17.0853 11.4665 17.0853 11.5983C17.0853 11.6313 17.0346 11.7457 16.9332 11.9415L13.5197 15.355C13.4412 15.4335 13.3601 15.4908 13.2764 15.5269C13.1926 15.563 13.1005 15.581 12.9999 15.581C12.8994 15.581 12.8073 15.563 12.7235 15.5269C12.6397 15.4908 12.5586 15.4335 12.4802 15.355Z" fill="#333236"/>
+</svg>
diff --git a/public/assets/image/check.svg b/public/assets/image/check.svg
new file mode 100644
index 0000000..17b720d
--- /dev/null
+++ b/public/assets/image/check.svg
@@ -0,0 +1,3 @@
+<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8.75443 14.2233L16.673 6.3047C16.8094 6.16838 16.9712 6.09874 17.1587 6.09579C17.3461 6.09286 17.5109 6.1625 17.6531 6.3047C17.7953 6.44689 17.8664 6.61024 17.8664 6.79475C17.8664 6.97924 17.7953 7.1426 17.6531 7.2848L9.33438 15.6035C9.16868 15.7692 8.97536 15.8521 8.75443 15.8521C8.53349 15.8521 8.34018 15.7692 8.17447 15.6035L4.34739 11.7765C4.21107 11.6401 4.14378 11.4783 4.14554 11.2908C4.14731 11.1034 4.21929 10.9386 4.36149 10.7964C4.50369 10.6542 4.66704 10.5831 4.85154 10.5831C5.03605 10.5831 5.1994 10.6542 5.34159 10.7964L8.75443 14.2233Z" fill="#787486"/>
+</svg>
diff --git a/src/components/dropdown/StatusDropdown.tsx b/src/components/dropdown/StatusDropdown.tsx
new file mode 100644
index 0000000..df16988
--- /dev/null
+++ b/src/components/dropdown/StatusDropdown.tsx
@@ -0,0 +1,119 @@
+import { useEffect, useRef, useState } from 'react'
+import Image from 'next/image'
+
+export enum Status {
+  TODO = 'To Do',
+  ON_PROGRESS = 'On Progress',
+  DONE = 'Done',
+}
+
+interface StatusDropdownProps {
+  value: Status
+  onChange: (status: Status) => void
+}
+
+const statusOptions: Status[] = [Status.TODO, Status.ON_PROGRESS, Status.DONE]
+
+export default function StatusDropdown({
+  value,
+  onChange,
+}: StatusDropdownProps) {
+  const [isOpen, setIsOpen] = useState(false)
+  const dropdownRef = useRef<HTMLDivElement>(null)
+
+  const handleSelect = (status: Status) => {
+    onChange(status)
+    setIsOpen(false)
+  }
+
+  useEffect(() => {
+    const handleKeyDown = (event: KeyboardEvent) => {
+      if (event.key === 'Escape' && isOpen) {
+        setIsOpen(false)
+        const activeElement = document.activeElement as HTMLElement
+        activeElement?.blur()
+      }
+    }
+
+    const handleClickOutside = (event: MouseEvent) => {
+      if (
+        dropdownRef.current &&
+        !dropdownRef.current.contains(event.target as Node)
+      ) {
+        setIsOpen(false)
+      }
+    }
+
+    document.addEventListener('keydown', handleKeyDown)
+    document.addEventListener('mousedown', handleClickOutside)
+
+    return () => {
+      document.removeEventListener('keydown', handleKeyDown)
+      document.removeEventListener('mousedown', handleClickOutside)
+    }
+  }, [isOpen])
+
+  return (
+    <div className="flex items-center justify-center min-h-screen">
+      <div
+        ref={dropdownRef}
+        className="relative inline-block w-full max-w-sm min-w-[200px] md:w-[217px]"
+      >
+        <button
+          onClick={() => setIsOpen((prev) => !prev)}
+          className={`w-full h-12 md:w-[217px] md:h-[48px] flex justify-between items-center px-4 py-3 rounded-[8px] text-[14px] font-medium bg-white focus:outline-none 
+          ${isOpen ? 'border-[#5534DA]' : 'border-[#D9D9D9]'} border`}
+        >
+          <div className="flex items-center gap-3">
+            <div className="inline-flex items-center gap-2 px-3 py-2 bg-[rgba(85,52,218,0.08)] rounded-full">
+              <span className="w-2 h-2 rounded-full bg-[#5534DA]" />
+              <span className="text-[#5534DA]">{value}</span>
+            </div>
+          </div>
+          <Image
+            src="/assets/image/arrow-down.svg"
+            alt="Arrow Down"
+            width={24}
+            height={24}
+            className={`transition-transform duration-300 ${
+              isOpen ? 'rotate-180' : ''
+            }`}
+          />
+        </button>
+
+        {isOpen && (
+          <ul className="absolute z-10 mt-2 w-full border border-[#D9D9D9] bg-white rounded-[8px] transition-all duration-300 origin-top md:w-[217px] max-h-60 overflow-y-auto">
+            {statusOptions.map((status) => (
+              <li
+                key={status}
+                onClick={() => handleSelect(status)}
+                onKeyDown={(e) => {
+                  if (e.key === 'Enter') {
+                    handleSelect(status)
+                  }
+                }}
+                tabIndex={0}
+                className="flex items-center gap-3 px-4 hover:bg-gray-100 cursor-pointer text-[14px] font-medium w-full h-12 md:h-[48px] focus:outline-none focus:bg-gray-200"
+              >
+                {value === status ? (
+                  <Image
+                    src="/assets/image/check.svg"
+                    alt="check"
+                    width={22}
+                    height={22}
+                  />
+                ) : (
+                  <div className="w-[22px] h-[22px]" />
+                )}
+                <div className="inline-flex items-center gap-2 px-3 py-2 bg-[rgba(85,52,218,0.08)] rounded-full">
+                  <span className="w-2 h-2 rounded-full bg-[#5534DA]" />
+                  <span className="text-[#5534DA]">{status}</span>
+                </div>
+              </li>
+            ))}
+          </ul>
+        )}
+      </div>
+    </div>
+  )
+}
diff --git a/src/components/dropdown/UserDropdown.tsx b/src/components/dropdown/UserDropdown.tsx
new file mode 100644
index 0000000..6c6b188
--- /dev/null
+++ b/src/components/dropdown/UserDropdown.tsx
@@ -0,0 +1,151 @@
+import { useEffect, useRef, useState, useCallback } from 'react'
+import Image from 'next/image'
+
+interface User {
+  id: number
+  name: string
+  badgeColor: string
+}
+
+interface UserDropdownProps {
+  users: User[]
+  selectedUser: User
+  onChange: (user: User) => void
+}
+
+export default function UserDropdown({
+  users,
+  selectedUser,
+  onChange,
+}: UserDropdownProps) {
+  const [isOpen, setIsOpen] = useState(false)
+  const dropdownRef = useRef<HTMLDivElement>(null)
+
+  const handleSelect = useCallback(
+    (user: User) => {
+      onChange(user)
+      setIsOpen(false)
+    },
+    [onChange]
+  )
+
+  const getInitial = useCallback((name: string) => {
+    if (!name) return 'U'
+    const firstChar = name.trim().charAt(0)
+    const isEnglish = /^[A-Za-z]$/.test(firstChar)
+    const hangulToRoman: Record<string, string> = {
+      하: 'H',
+      배: 'B',
+      김: 'K',
+      이: 'L',
+      박: 'P',
+      최: 'C',
+      정: 'J',
+      강: 'K',
+      조: 'J',
+      윤: 'Y',
+      장: 'J',
+    }
+    return isEnglish ? firstChar.toUpperCase() : hangulToRoman[firstChar] || 'U'
+  }, [])
+
+  useEffect(() => {
+    const handleKeyDown = (event: KeyboardEvent) => {
+      if (event.key === 'Escape' && isOpen) {
+        setIsOpen(false)
+        ;(document.activeElement as HTMLElement)?.blur()
+      }
+    }
+
+    const handleClickOutside = (event: MouseEvent) => {
+      if (
+        dropdownRef.current &&
+        !dropdownRef.current.contains(event.target as Node)
+      ) {
+        setIsOpen(false)
+      }
+    }
+
+    document.addEventListener('keydown', handleKeyDown)
+    document.addEventListener('mousedown', handleClickOutside)
+
+    return () => {
+      document.removeEventListener('keydown', handleKeyDown)
+      document.removeEventListener('mousedown', handleClickOutside)
+    }
+  }, [isOpen])
+
+  return (
+    <div className="flex items-center justify-center min-h-screen p-4">
+      <div
+        ref={dropdownRef}
+        className="relative inline-block w-full max-w-sm min-w-[200px] md:w-[217px]"
+      >
+        {/* 버튼 */}
+        <button
+          type="button"
+          onClick={() => setIsOpen((prev) => !prev)}
+          className={`w-full h-12 md:w-[217px] md:h-[48px] flex justify-between items-center px-4 py-3 rounded-[8px] text-[16px] font-regular bg-white focus:outline-none
+          ${isOpen ? 'border-[#5534DA]' : 'border-[#D9D9D9]'} border`}
+        >
+          <div className="flex items-center gap-3">
+            <div
+              style={{ backgroundColor: selectedUser.badgeColor }}
+              className="w-[26px] h-[26px] flex items-center justify-center rounded-full text-white text-[16px] font-regular"
+            >
+              {getInitial(selectedUser.name)}
+            </div>
+            <span className="truncate">{selectedUser.name}</span>
+          </div>
+          <Image
+            src="/assets/image/arrow-down.svg"
+            alt="Arrow Down"
+            width={24}
+            height={24}
+            className={`transition-transform duration-300 ${
+              isOpen ? 'rotate-180' : ''
+            }`}
+          />
+        </button>
+
+        {/* 드롭다운 리스트 */}
+        {isOpen && (
+          <ul className="absolute z-10 mt-2 w-full border border-[#D9D9D9] bg-white rounded-[8px] transition-all duration-300 origin-top md:w-[217px] max-h-60 overflow-y-auto">
+            {users.map((user) => (
+              <li
+                key={user.id}
+                onClick={() => handleSelect(user)}
+                onKeyDown={(e) => e.key === 'Enter' && handleSelect(user)}
+                tabIndex={0}
+                className="flex items-center gap-3 px-4 hover:bg-gray-100 cursor-pointer text-[16px] font-regular w-full h-12 md:h-[48px] focus:outline-none focus:bg-gray-200"
+              >
+                {/* 체크 표시 */}
+                {selectedUser.id === user.id ? (
+                  <Image
+                    src="/assets/image/check.svg"
+                    alt="check"
+                    width={22}
+                    height={22}
+                  />
+                ) : (
+                  <div className="w-[22px] h-[22px]" />
+                )}
+
+                {/* 유저 이름 + 뱃지 */}
+                <div className="flex items-center gap-3 flex-1">
+                  <div
+                    style={{ backgroundColor: user.badgeColor }}
+                    className="w-[26px] h-[26px] flex items-center justify-center rounded-full text-white text-[16px] font-regular"
+                  >
+                    {getInitial(user.name)}
+                  </div>
+                  <span className="truncate">{user.name}</span>
+                </div>
+              </li>
+            ))}
+          </ul>
+        )}
+      </div>
+    </div>
+  )
+}


Reviews:
user: login: ramong26
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-27T15:37:00Z
user: login: ramong26
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-27T15:49:00Z
user: login: huiseong29
body: 타입 명시와 컴포넌트 구조, 상태 관리 흐름이 명확해서 가독성이 좋고, 테스트용 페이지로서 필요한 기능을 빠짐없이 잘 구현해주셨습니다. 불필요한 최적화 없이 필요한 부분만 간결하게 작성한 것도 잘된 부분 같네요 😃 

코멘트 확인해보시고 수정된 부분에는 어떤 식으로 수정했는데 답글 달아주시면 확인 후에 승인 처리시키도록 하겠습니다!! 고생 많으셨습니다~
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-27T23:55:12Z
user: login: Parkchanyoung0710
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-28T03:11:42Z
user: login: Parkchanyoung0710
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-28T03:14:45Z
user: login: Parkchanyoung0710
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-28T03:15:11Z
user: login: Parkchanyoung0710
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-28T03:21:48Z
user: login: Parkchanyoung0710
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-28T04:24:24Z
user: login: Parkchanyoung0710
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-28T04:24:41Z
user: login: Parkchanyoung0710
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-28T04:28:43Z
user: login: huiseong29
body: 고생 많으셨습니다~
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-28T06:13:27Z
user: login: dkozowlk
body: 고생하셨습니다!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-28T06:32:46Z

Review comments:
diff_hunk: @@ -0,0 +1,101 @@
+import { useEffect, useRef, useState } from 'react'
+import Image from 'next/image'
+
+// API에서 상태 데이터를 받아와서 사용해야함
+type Status = 'To Do' | 'On Progress' | 'Done'
+
+interface StatusDropdownProps {
+  value: Status
+  onChange: (status: Status) => void
+}
+
+const statusOptions: Status[] = ['To Do', 'On Progress', 'Done']
+
+export default function StatusDropdown({
+  value,
+  onChange,
+}: StatusDropdownProps) {
+  const [isOpen, setIsOpen] = useState(false)
+  const dropdownRef = useRef<HTMLDivElement>(null)
+
+  const handleSelect = (status: Status) => {
+    onChange(status)
+    setIsOpen(false)
+  }
+
+  useEffect(() => {
+    const handleKeyDown = (event: KeyboardEvent) => {
+      if (event.key === 'Escape' && isOpen) {
+        setIsOpen(false)
+
+        // 포커스 해제
+        const activeElement = document.activeElement as HTMLElement
+        activeElement?.blur()
+      }
+    }
+
+    document.addEventListener('keydown', handleKeyDown)
+
+    return () => {
+      document.removeEventListener('keydown', handleKeyDown)
+    }
+  }, [isOpen])
+  return (
+    <div className="flex items-center justify-center min-h-screen">
+      <div
+        ref={dropdownRef}
+        className="relative inline-block w-full max-w-sm min-w-[200px] md:w-[217px]"
+      >
+        <button
+          onClick={() => setIsOpen((prev) => !prev)}
+          className={`w-full h-12 md:w-[217px] md:h-[48px] flex justify-between items-center px-4 py-3 rounded-md text-lg bg-white focus:outline-none 
+    ${isOpen ? 'border-[#5534DA]' : 'border-[#D9D9D9]'} border`}
+        >
+          <div className="flex items-center gap-3">
+            <div className="inline-flex items-center gap-2 px-3 py-2 bg-[#F1E5FF] rounded-full">
+              <span className="w-2 h-2 rounded-full bg-[#5534DA]" />
+              <span className="text-[#5534DA]">{value}</span>
path: src/components/dropdown/StatusDropdown.tsx
user: login: ramong26
body: font 사이즈 확인 부탁드립니다
created_at: 2025-04-27T15:26:54Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,101 @@
+import { useEffect, useRef, useState } from 'react'
+import Image from 'next/image'
+
+// API에서 상태 데이터를 받아와서 사용해야함
+type Status = 'To Do' | 'On Progress' | 'Done'
+
+interface StatusDropdownProps {
+  value: Status
+  onChange: (status: Status) => void
+}
+
+const statusOptions: Status[] = ['To Do', 'On Progress', 'Done']
+
+export default function StatusDropdown({
+  value,
+  onChange,
+}: StatusDropdownProps) {
+  const [isOpen, setIsOpen] = useState(false)
+  const dropdownRef = useRef<HTMLDivElement>(null)
+
+  const handleSelect = (status: Status) => {
+    onChange(status)
+    setIsOpen(false)
+  }
+
+  useEffect(() => {
+    const handleKeyDown = (event: KeyboardEvent) => {
+      if (event.key === 'Escape' && isOpen) {
+        setIsOpen(false)
+
+        // 포커스 해제
+        const activeElement = document.activeElement as HTMLElement
+        activeElement?.blur()
+      }
+    }
+
+    document.addEventListener('keydown', handleKeyDown)
+
+    return () => {
+      document.removeEventListener('keydown', handleKeyDown)
+    }
+  }, [isOpen])
+  return (
+    <div className="flex items-center justify-center min-h-screen">
+      <div
+        ref={dropdownRef}
+        className="relative inline-block w-full max-w-sm min-w-[200px] md:w-[217px]"
+      >
+        <button
+          onClick={() => setIsOpen((prev) => !prev)}
+          className={`w-full h-12 md:w-[217px] md:h-[48px] flex justify-between items-center px-4 py-3 rounded-md text-lg bg-white focus:outline-none 
+    ${isOpen ? 'border-[#5534DA]' : 'border-[#D9D9D9]'} border`}
+        >
+          <div className="flex items-center gap-3">
+            <div className="inline-flex items-center gap-2 px-3 py-2 bg-[#F1E5FF] rounded-full">
path: src/components/dropdown/StatusDropdown.tsx
user: login: ramong26
body: 백그라운드 컬러 맞지 않네요 확인 부탁드립니다..!
created_at: 2025-04-27T15:30:49Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,101 @@
+import { useEffect, useRef, useState } from 'react'
+import Image from 'next/image'
+
+// API에서 상태 데이터를 받아와서 사용해야함
+type Status = 'To Do' | 'On Progress' | 'Done'
+
+interface StatusDropdownProps {
+  value: Status
+  onChange: (status: Status) => void
+}
+
+const statusOptions: Status[] = ['To Do', 'On Progress', 'Done']
+
+export default function StatusDropdown({
+  value,
+  onChange,
+}: StatusDropdownProps) {
+  const [isOpen, setIsOpen] = useState(false)
+  const dropdownRef = useRef<HTMLDivElement>(null)
+
+  const handleSelect = (status: Status) => {
+    onChange(status)
+    setIsOpen(false)
+  }
+
+  useEffect(() => {
+    const handleKeyDown = (event: KeyboardEvent) => {
+      if (event.key === 'Escape' && isOpen) {
+        setIsOpen(false)
+
+        // 포커스 해제
+        const activeElement = document.activeElement as HTMLElement
+        activeElement?.blur()
+      }
+    }
+
+    document.addEventListener('keydown', handleKeyDown)
+
+    return () => {
+      document.removeEventListener('keydown', handleKeyDown)
+    }
+  }, [isOpen])
+  return (
+    <div className="flex items-center justify-center min-h-screen">
+      <div
+        ref={dropdownRef}
+        className="relative inline-block w-full max-w-sm min-w-[200px] md:w-[217px]"
+      >
+        <button
+          onClick={() => setIsOpen((prev) => !prev)}
+          className={`w-full h-12 md:w-[217px] md:h-[48px] flex justify-between items-center px-4 py-3 rounded-md text-lg bg-white focus:outline-none 
path: src/components/dropdown/StatusDropdown.tsx
user: login: ramong26
body: 셀렉트 박스랑 전체적인 박스 둘다 border-radius가 맞지 않습니다 이것도 확인 부탁드려요..!
created_at: 2025-04-27T15:32:59Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,135 @@
+import { useEffect, useRef, useState, useCallback } from 'react'
+import Image from 'next/image'
+
+interface User {
+  id: number
+  name: string
+  badgeColor: string
+}
+
+interface UserDropdownProps {
+  users: User[]
+  selectedUser: User
+  onChange: (user: User) => void
+}
+
+export default function UserDropdown({
+  users,
+  selectedUser,
+  onChange,
+}: UserDropdownProps) {
+  const [isOpen, setIsOpen] = useState(false)
+  const dropdownRef = useRef<HTMLDivElement>(null)
+
+  const handleSelect = useCallback(
+    (user: User) => {
+      onChange(user)
+      setIsOpen(false)
+    },
+    [onChange]
+  )
+
+  const getInitial = useCallback((name: string) => {
+    if (!name) return 'U'
+    const firstChar = name.trim().charAt(0)
+    const isEnglish = /^[A-Za-z]$/.test(firstChar)
+    // API 서버에서 이름을 가져와서 사용해야함
+    const hangulToRoman: { [key: string]: string } = {
+      하: 'H',
+      배: 'B',
+      김: 'K',
+      이: 'L',
+      박: 'P',
+      최: 'C',
+      정: 'J',
+      강: 'K',
+      조: 'J',
+      윤: 'Y',
+      장: 'J',
+    }
+
+    return isEnglish ? firstChar.toUpperCase() : hangulToRoman[firstChar] || 'U'
+  }, [])
+
+  useEffect(() => {
+    const handleKeyDown = (event: KeyboardEvent) => {
+      if (event.key === 'Escape' && isOpen) {
+        setIsOpen(false)
+
+        // 포커스 해제
+        const activeElement = document.activeElement as HTMLElement
+        activeElement?.blur()
+      }
+    }
+
+    document.addEventListener('keydown', handleKeyDown)
+
+    return () => {
+      document.removeEventListener('keydown', handleKeyDown)
+    }
+  }, [isOpen])
+
+  return (
+    <div className="flex items-center justify-center min-h-screen p-4">
+      <div
+        ref={dropdownRef}
+        className="relative inline-block w-full max-w-sm min-w-[200px] md:w-[217px]"
+      >
+        <button
+          onClick={() => setIsOpen((prev) => !prev)}
+          className={`w-full h-12 md:w-[217px] md:h-[48px] flex justify-between items-center px-4 py-3 rounded-md text-lg bg-white focus:outline-none 
+    ${isOpen ? 'border-[#5534DA]' : 'border-[#D9D9D9]'} border`}
+        >
+          <div className="flex items-center gap-3">
+            <div
+              className={`w-8 h-8 flex items-center justify-center rounded-full text-white text-sm ${selectedUser.badgeColor}`}
+            >
+              {getInitial(selectedUser.name)}
+            </div>
+            <span className="truncate">{selectedUser.name}</span>
path: src/components/dropdown/UserDropdown.tsx
user: login: ramong26
body: 폰트 사이즈 + weight 확인 부탁드립ㄴ다 
created_at: 2025-04-27T15:34:41Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,135 @@
+import { useEffect, useRef, useState, useCallback } from 'react'
+import Image from 'next/image'
+
+interface User {
+  id: number
+  name: string
+  badgeColor: string
+}
+
+interface UserDropdownProps {
+  users: User[]
+  selectedUser: User
+  onChange: (user: User) => void
+}
+
+export default function UserDropdown({
+  users,
+  selectedUser,
+  onChange,
+}: UserDropdownProps) {
+  const [isOpen, setIsOpen] = useState(false)
+  const dropdownRef = useRef<HTMLDivElement>(null)
+
+  const handleSelect = useCallback(
+    (user: User) => {
+      onChange(user)
+      setIsOpen(false)
+    },
+    [onChange]
+  )
+
+  const getInitial = useCallback((name: string) => {
+    if (!name) return 'U'
+    const firstChar = name.trim().charAt(0)
+    const isEnglish = /^[A-Za-z]$/.test(firstChar)
+    // API 서버에서 이름을 가져와서 사용해야함
+    const hangulToRoman: { [key: string]: string } = {
+      하: 'H',
+      배: 'B',
+      김: 'K',
+      이: 'L',
+      박: 'P',
+      최: 'C',
+      정: 'J',
+      강: 'K',
+      조: 'J',
+      윤: 'Y',
+      장: 'J',
+    }
+
+    return isEnglish ? firstChar.toUpperCase() : hangulToRoman[firstChar] || 'U'
+  }, [])
+
+  useEffect(() => {
+    const handleKeyDown = (event: KeyboardEvent) => {
+      if (event.key === 'Escape' && isOpen) {
+        setIsOpen(false)
+
+        // 포커스 해제
+        const activeElement = document.activeElement as HTMLElement
+        activeElement?.blur()
+      }
+    }
+
+    document.addEventListener('keydown', handleKeyDown)
+
+    return () => {
+      document.removeEventListener('keydown', handleKeyDown)
+    }
+  }, [isOpen])
+
+  return (
+    <div className="flex items-center justify-center min-h-screen p-4">
+      <div
+        ref={dropdownRef}
+        className="relative inline-block w-full max-w-sm min-w-[200px] md:w-[217px]"
+      >
+        <button
+          onClick={() => setIsOpen((prev) => !prev)}
+          className={`w-full h-12 md:w-[217px] md:h-[48px] flex justify-between items-center px-4 py-3 rounded-md text-lg bg-white focus:outline-none 
+    ${isOpen ? 'border-[#5534DA]' : 'border-[#D9D9D9]'} border`}
+        >
+          <div className="flex items-center gap-3">
+            <div
+              className={`w-8 h-8 flex items-center justify-center rounded-full text-white text-sm ${selectedUser.badgeColor}`}
+            >
+              {getInitial(selectedUser.name)}
+            </div>
+            <span className="truncate">{selectedUser.name}</span>
+          </div>
+          <Image
+            src="/assets/image/arrow-down.svg"
+            alt="Arrow Down"
+            width={24}
+            height={24}
+            className={`transition-transform duration-300 ${
+              isOpen ? 'rotate-180' : ''
+            }`}
+          />
+        </button>
+
+        {isOpen && (
+          <ul className="absolute z-10 mt-2 w-full border border-[#D9D9D9] bg-white rounded-md transition-all duration-300 origin-top md:w-[217px] max-h-60 overflow-y-auto">
+            {users.map((user) => (
+              <li
+                key={user.id}
+                onClick={() => handleSelect(user)}
+                className="flex items-center gap-3 px-4 hover:bg-gray-100 cursor-pointer text-lg w-full h-12 md:h-[48px]"
+              >
+                {selectedUser.id === user.id ? (
+                  <Image
+                    src="/assets/image/check.svg"
+                    alt="check"
+                    width={22}
+                    height={22}
+                  />
+                ) : (
+                  <div className="w-[22px] h-[22px]" />
+                )}
+                <div className="flex items-center gap-3 flex-1">
+                  <div
+                    className={`w-8 h-8 flex items-center justify-center rounded-full text-white text-sm ${user.badgeColor}`}
path: src/components/dropdown/UserDropdown.tsx
user: login: ramong26
body: 뱃지 사이즈가 맞지 않은데 이건 나중에 컴포넌트 만들고 수정하죠..,! 일단 사이즈는 맞지 않습니다..! 
created_at: 2025-04-27T15:36:31Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,101 @@
+import { useEffect, useRef, useState } from 'react'
+import Image from 'next/image'
+
+// API에서 상태 데이터를 받아와서 사용해야함
+type Status = 'To Do' | 'On Progress' | 'Done'
+
+interface StatusDropdownProps {
+  value: Status
+  onChange: (status: Status) => void
+}
+
+const statusOptions: Status[] = ['To Do', 'On Progress', 'Done']
+
+export default function StatusDropdown({
+  value,
+  onChange,
+}: StatusDropdownProps) {
+  const [isOpen, setIsOpen] = useState(false)
+  const dropdownRef = useRef<HTMLDivElement>(null)
+
+  const handleSelect = (status: Status) => {
+    onChange(status)
+    setIsOpen(false)
+  }
+
+  useEffect(() => {
path: src/components/dropdown/StatusDropdown.tsx
user: login: ramong26
body: 하신게 많네요.. 고생하셨어요.

useEffect가 esc를 누르면 닫게 해주는데 이거 말고 화면 어딘가 다른 곳을 누르면 닫히도록 해주게 더 좋을듯싶습니다..!
created_at: 2025-04-27T15:46:52Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,101 @@
+import { useEffect, useRef, useState } from 'react'
+import Image from 'next/image'
+
+// API에서 상태 데이터를 받아와서 사용해야함
+type Status = 'To Do' | 'On Progress' | 'Done'
+
+interface StatusDropdownProps {
+  value: Status
+  onChange: (status: Status) => void
+}
+
+const statusOptions: Status[] = ['To Do', 'On Progress', 'Done']
path: src/components/dropdown/StatusDropdown.tsx
user: login: huiseong29
body: Status 관리는 enum을 활용해보시는 게 어떠신가요?
타입 안정성 향상과 추후 확장시 용이할 것 같아요
created_at: 2025-04-27T23:45:12Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,101 @@
+import { useEffect, useRef, useState } from 'react'
+import Image from 'next/image'
+
+// API에서 상태 데이터를 받아와서 사용해야함
+type Status = 'To Do' | 'On Progress' | 'Done'
+
+interface StatusDropdownProps {
+  value: Status
+  onChange: (status: Status) => void
+}
+
+const statusOptions: Status[] = ['To Do', 'On Progress', 'Done']
+
+export default function StatusDropdown({
+  value,
+  onChange,
+}: StatusDropdownProps) {
+  const [isOpen, setIsOpen] = useState(false)
+  const dropdownRef = useRef<HTMLDivElement>(null)
+
+  const handleSelect = (status: Status) => {
+    onChange(status)
+    setIsOpen(false)
+  }
+
+  useEffect(() => {
path: src/components/dropdown/StatusDropdown.tsx
user: login: huiseong29
body: 저도 동의하는 바입니다. mousedown을 고려해보시는 건 어떠신가요?
created_at: 2025-04-27T23:45:39Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,101 @@
+import { useEffect, useRef, useState } from 'react'
+import Image from 'next/image'
+
+// API에서 상태 데이터를 받아와서 사용해야함
+type Status = 'To Do' | 'On Progress' | 'Done'
+
+interface StatusDropdownProps {
+  value: Status
+  onChange: (status: Status) => void
+}
+
+const statusOptions: Status[] = ['To Do', 'On Progress', 'Done']
+
+export default function StatusDropdown({
+  value,
+  onChange,
+}: StatusDropdownProps) {
+  const [isOpen, setIsOpen] = useState(false)
+  const dropdownRef = useRef<HTMLDivElement>(null)
+
+  const handleSelect = (status: Status) => {
+    onChange(status)
+    setIsOpen(false)
+  }
+
+  useEffect(() => {
+    const handleKeyDown = (event: KeyboardEvent) => {
+      if (event.key === 'Escape' && isOpen) {
+        setIsOpen(false)
+
+        // 포커스 해제
+        const activeElement = document.activeElement as HTMLElement
+        activeElement?.blur()
+      }
+    }
+
+    document.addEventListener('keydown', handleKeyDown)
+
+    return () => {
+      document.removeEventListener('keydown', handleKeyDown)
+    }
+  }, [isOpen])
+  return (
+    <div className="flex items-center justify-center min-h-screen">
+      <div
+        ref={dropdownRef}
+        className="relative inline-block w-full max-w-sm min-w-[200px] md:w-[217px]"
+      >
+        <button
+          onClick={() => setIsOpen((prev) => !prev)}
+          className={`w-full h-12 md:w-[217px] md:h-[48px] flex justify-between items-center px-4 py-3 rounded-md text-lg bg-white focus:outline-none 
+    ${isOpen ? 'border-[#5534DA]' : 'border-[#D9D9D9]'} border`}
+        >
+          <div className="flex items-center gap-3">
+            <div className="inline-flex items-center gap-2 px-3 py-2 bg-[#F1E5FF] rounded-full">
+              <span className="w-2 h-2 rounded-full bg-[#5534DA]" />
+              <span className="text-[#5534DA]">{value}</span>
+            </div>
+          </div>
+          <Image
+            src="/assets/image/arrow-down.svg"
+            alt="Arrow Down"
+            width={24}
+            height={24}
+            className={`transition-transform duration-300 ${
+              isOpen ? 'rotate-180' : '' // 180도로 회전 시킴
+            }`}
+          />
+        </button>
+
+        {isOpen && (
+          <ul className="absolute z-10 mt-2 w-full border border-[#D9D9D9] bg-white rounded-md transition-all duration-300 origin-top md:w-[217px] max-h-60 overflow-y-auto">
+            {statusOptions.map((status) => (
+              <li
+                key={status}
+                onClick={() => handleSelect(status)}
+                className="flex items-center gap-3 px-4 hover:bg-gray-100 cursor-pointer text-lg w-full h-12 md:h-[48px]"
+              >
+                {value === status ? (
+                  <Image
+                    src="/assets/image/check.svg"
+                    alt="check"
+                    width={22}
+                    height={22}
+                  />
+                ) : (
+                  <div className="w-[22px] h-[22px]" />
+                )}
+
+                <div className="inline-flex items-center gap-2 px-3 py-2 bg-[#F1E5FF] rounded-full">
+                  <span className="w-2 h-2 rounded-full bg-[#5534DA]" />
+                  <span className="text-[#5534DA]">{status}</span>
+                </div>
+              </li>
+            ))}
+          </ul>
+        )}
+      </div>
+    </div>
+  )
+}
path: src/components/dropdown/StatusDropdown.tsx
user: login: huiseong29
body: tab으로도 이동 가능하게 하려면 tabIndex와 onKeyDown을 추가하면 된다고 하네요

created_at: 2025-04-27T23:48:30Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,135 @@
+import { useEffect, useRef, useState, useCallback } from 'react'
+import Image from 'next/image'
+
+interface User {
+  id: number
+  name: string
+  badgeColor: string
+}
+
+interface UserDropdownProps {
+  users: User[]
+  selectedUser: User
+  onChange: (user: User) => void
+}
+
+export default function UserDropdown({
+  users,
+  selectedUser,
+  onChange,
+}: UserDropdownProps) {
+  const [isOpen, setIsOpen] = useState(false)
+  const dropdownRef = useRef<HTMLDivElement>(null)
+
+  const handleSelect = useCallback(
+    (user: User) => {
+      onChange(user)
+      setIsOpen(false)
+    },
+    [onChange]
+  )
+
+  const getInitial = useCallback((name: string) => {
+    if (!name) return 'U'
+    const firstChar = name.trim().charAt(0)
+    const isEnglish = /^[A-Za-z]$/.test(firstChar)
+    // API 서버에서 이름을 가져와서 사용해야함
+    const hangulToRoman: { [key: string]: string } = {
+      하: 'H',
+      배: 'B',
+      김: 'K',
+      이: 'L',
+      박: 'P',
+      최: 'C',
+      정: 'J',
+      강: 'K',
+      조: 'J',
+      윤: 'Y',
+      장: 'J',
+    }
+
+    return isEnglish ? firstChar.toUpperCase() : hangulToRoman[firstChar] || 'U'
+  }, [])
+
+  useEffect(() => {
+    const handleKeyDown = (event: KeyboardEvent) => {
+      if (event.key === 'Escape' && isOpen) {
+        setIsOpen(false)
+
+        // 포커스 해제
+        const activeElement = document.activeElement as HTMLElement
+        activeElement?.blur()
+      }
+    }
+
+    document.addEventListener('keydown', handleKeyDown)
+
+    return () => {
+      document.removeEventListener('keydown', handleKeyDown)
+    }
+  }, [isOpen])
+
+  return (
+    <div className="flex items-center justify-center min-h-screen p-4">
+      <div
+        ref={dropdownRef}
+        className="relative inline-block w-full max-w-sm min-w-[200px] md:w-[217px]"
+      >
+        <button
+          onClick={() => setIsOpen((prev) => !prev)}
+          className={`w-full h-12 md:w-[217px] md:h-[48px] flex justify-between items-center px-4 py-3 rounded-md text-lg bg-white focus:outline-none 
+    ${isOpen ? 'border-[#5534DA]' : 'border-[#D9D9D9]'} border`}
+        >
+          <div className="flex items-center gap-3">
+            <div
+              className={`w-8 h-8 flex items-center justify-center rounded-full text-white text-sm ${selectedUser.badgeColor}`}
+            >
+              {getInitial(selectedUser.name)}
+            </div>
+            <span className="truncate">{selectedUser.name}</span>
+          </div>
+          <Image
+            src="/assets/image/arrow-down.svg"
+            alt="Arrow Down"
+            width={24}
+            height={24}
+            className={`transition-transform duration-300 ${
+              isOpen ? 'rotate-180' : ''
+            }`}
+          />
+        </button>
+
+        {isOpen && (
+          <ul className="absolute z-10 mt-2 w-full border border-[#D9D9D9] bg-white rounded-md transition-all duration-300 origin-top md:w-[217px] max-h-60 overflow-y-auto">
+            {users.map((user) => (
+              <li
+                key={user.id}
+                onClick={() => handleSelect(user)}
+                className="flex items-center gap-3 px-4 hover:bg-gray-100 cursor-pointer text-lg w-full h-12 md:h-[48px]"
+              >
+                {selectedUser.id === user.id ? (
+                  <Image
+                    src="/assets/image/check.svg"
+                    alt="check"
+                    width={22}
+                    height={22}
+                  />
+                ) : (
+                  <div className="w-[22px] h-[22px]" />
+                )}
+                <div className="flex items-center gap-3 flex-1">
+                  <div
+                    className={`w-8 h-8 flex items-center justify-center rounded-full text-white text-sm ${user.badgeColor}`}
+                  >
+                    {getInitial(user.name)}
+                  </div>
+                  <span className="truncate">{user.name}</span>
+                </div>
+              </li>
+            ))}
+          </ul>
+        )}
+      </div>
+    </div>
+  )
+}
path: src/components/dropdown/UserDropdown.tsx
user: login: huiseong29
body: 여기 파일에서도 외부 클릭 시 드롭다운 닫기의 기능이 추가되면 더 좋은 결과물이 될 것 같습니다.
created_at: 2025-04-27T23:50:23Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,135 @@
+import { useEffect, useRef, useState, useCallback } from 'react'
+import Image from 'next/image'
+
+interface User {
+  id: number
+  name: string
+  badgeColor: string
+}
+
+interface UserDropdownProps {
+  users: User[]
+  selectedUser: User
+  onChange: (user: User) => void
+}
+
+export default function UserDropdown({
+  users,
+  selectedUser,
+  onChange,
+}: UserDropdownProps) {
+  const [isOpen, setIsOpen] = useState(false)
+  const dropdownRef = useRef<HTMLDivElement>(null)
+
+  const handleSelect = useCallback(
+    (user: User) => {
+      onChange(user)
+      setIsOpen(false)
+    },
+    [onChange]
+  )
+
+  const getInitial = useCallback((name: string) => {
+    if (!name) return 'U'
+    const firstChar = name.trim().charAt(0)
+    const isEnglish = /^[A-Za-z]$/.test(firstChar)
+    // API 서버에서 이름을 가져와서 사용해야함
+    const hangulToRoman: { [key: string]: string } = {
+      하: 'H',
+      배: 'B',
+      김: 'K',
+      이: 'L',
+      박: 'P',
+      최: 'C',
+      정: 'J',
+      강: 'K',
+      조: 'J',
+      윤: 'Y',
+      장: 'J',
+    }
+
+    return isEnglish ? firstChar.toUpperCase() : hangulToRoman[firstChar] || 'U'
+  }, [])
+
+  useEffect(() => {
+    const handleKeyDown = (event: KeyboardEvent) => {
+      if (event.key === 'Escape' && isOpen) {
+        setIsOpen(false)
+
+        // 포커스 해제
+        const activeElement = document.activeElement as HTMLElement
+        activeElement?.blur()
+      }
+    }
+
+    document.addEventListener('keydown', handleKeyDown)
+
+    return () => {
+      document.removeEventListener('keydown', handleKeyDown)
+    }
+  }, [isOpen])
+
+  return (
+    <div className="flex items-center justify-center min-h-screen p-4">
+      <div
+        ref={dropdownRef}
+        className="relative inline-block w-full max-w-sm min-w-[200px] md:w-[217px]"
+      >
+        <button
+          onClick={() => setIsOpen((prev) => !prev)}
+          className={`w-full h-12 md:w-[217px] md:h-[48px] flex justify-between items-center px-4 py-3 rounded-md text-lg bg-white focus:outline-none 
+    ${isOpen ? 'border-[#5534DA]' : 'border-[#D9D9D9]'} border`}
+        >
+          <div className="flex items-center gap-3">
+            <div
+              className={`w-8 h-8 flex items-center justify-center rounded-full text-white text-sm ${selectedUser.badgeColor}`}
+            >
+              {getInitial(selectedUser.name)}
+            </div>
+            <span className="truncate">{selectedUser.name}</span>
+          </div>
+          <Image
+            src="/assets/image/arrow-down.svg"
+            alt="Arrow Down"
+            width={24}
+            height={24}
+            className={`transition-transform duration-300 ${
+              isOpen ? 'rotate-180' : ''
+            }`}
+          />
+        </button>
+
+        {isOpen && (
+          <ul className="absolute z-10 mt-2 w-full border border-[#D9D9D9] bg-white rounded-md transition-all duration-300 origin-top md:w-[217px] max-h-60 overflow-y-auto">
+            {users.map((user) => (
+              <li
+                key={user.id}
+                onClick={() => handleSelect(user)}
+                className="flex items-center gap-3 px-4 hover:bg-gray-100 cursor-pointer text-lg w-full h-12 md:h-[48px]"
path: src/components/dropdown/UserDropdown.tsx
user: login: huiseong29
body: ```
className={`flex items-center gap-3 px-4 cursor-pointer text-lg w-full h-12 md:h-[48px] ${
    selectedUser.id === user.id ? 'bg-violet-100 font-semibold' : 'hover:bg-gray-100'
  }`}
```
이런 느낌으로 확실한 구분을 주는 것도 좋은 방법이 될 수 있을 것 같아요
created_at: 2025-04-27T23:52:38Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,101 @@
+import { useEffect, useRef, useState } from 'react'
+import Image from 'next/image'
+
+// API에서 상태 데이터를 받아와서 사용해야함
+type Status = 'To Do' | 'On Progress' | 'Done'
+
+interface StatusDropdownProps {
+  value: Status
+  onChange: (status: Status) => void
+}
+
+const statusOptions: Status[] = ['To Do', 'On Progress', 'Done']
+
+export default function StatusDropdown({
+  value,
+  onChange,
+}: StatusDropdownProps) {
+  const [isOpen, setIsOpen] = useState(false)
+  const dropdownRef = useRef<HTMLDivElement>(null)
+
+  const handleSelect = (status: Status) => {
+    onChange(status)
+    setIsOpen(false)
+  }
+
+  useEffect(() => {
path: src/components/dropdown/StatusDropdown.tsx
user: login: Parkchanyoung0710
body: 저도 같은 생각으로 mousedown은 만들었다가 피그마처럼 보여주기 위해서 삭제 했는데 다시 넣도록 하겠습니다.
created_at: 2025-04-28T03:11:42Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,101 @@
+import { useEffect, useRef, useState } from 'react'
+import Image from 'next/image'
+
+// API에서 상태 데이터를 받아와서 사용해야함
+type Status = 'To Do' | 'On Progress' | 'Done'
+
+interface StatusDropdownProps {
+  value: Status
+  onChange: (status: Status) => void
+}
+
+const statusOptions: Status[] = ['To Do', 'On Progress', 'Done']
path: src/components/dropdown/StatusDropdown.tsx
user: login: Parkchanyoung0710
body: 좋은 의견입니다😊
created_at: 2025-04-28T03:14:45Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,101 @@
+import { useEffect, useRef, useState } from 'react'
+import Image from 'next/image'
+
+// API에서 상태 데이터를 받아와서 사용해야함
+type Status = 'To Do' | 'On Progress' | 'Done'
+
+interface StatusDropdownProps {
+  value: Status
+  onChange: (status: Status) => void
+}
+
+const statusOptions: Status[] = ['To Do', 'On Progress', 'Done']
+
+export default function StatusDropdown({
+  value,
+  onChange,
+}: StatusDropdownProps) {
+  const [isOpen, setIsOpen] = useState(false)
+  const dropdownRef = useRef<HTMLDivElement>(null)
+
+  const handleSelect = (status: Status) => {
+    onChange(status)
+    setIsOpen(false)
+  }
+
+  useEffect(() => {
+    const handleKeyDown = (event: KeyboardEvent) => {
+      if (event.key === 'Escape' && isOpen) {
+        setIsOpen(false)
+
+        // 포커스 해제
+        const activeElement = document.activeElement as HTMLElement
+        activeElement?.blur()
+      }
+    }
+
+    document.addEventListener('keydown', handleKeyDown)
+
+    return () => {
+      document.removeEventListener('keydown', handleKeyDown)
+    }
+  }, [isOpen])
+  return (
+    <div className="flex items-center justify-center min-h-screen">
+      <div
+        ref={dropdownRef}
+        className="relative inline-block w-full max-w-sm min-w-[200px] md:w-[217px]"
+      >
+        <button
+          onClick={() => setIsOpen((prev) => !prev)}
+          className={`w-full h-12 md:w-[217px] md:h-[48px] flex justify-between items-center px-4 py-3 rounded-md text-lg bg-white focus:outline-none 
+    ${isOpen ? 'border-[#5534DA]' : 'border-[#D9D9D9]'} border`}
+        >
+          <div className="flex items-center gap-3">
+            <div className="inline-flex items-center gap-2 px-3 py-2 bg-[#F1E5FF] rounded-full">
+              <span className="w-2 h-2 rounded-full bg-[#5534DA]" />
+              <span className="text-[#5534DA]">{value}</span>
+            </div>
+          </div>
+          <Image
+            src="/assets/image/arrow-down.svg"
+            alt="Arrow Down"
+            width={24}
+            height={24}
+            className={`transition-transform duration-300 ${
+              isOpen ? 'rotate-180' : '' // 180도로 회전 시킴
+            }`}
+          />
+        </button>
+
+        {isOpen && (
+          <ul className="absolute z-10 mt-2 w-full border border-[#D9D9D9] bg-white rounded-md transition-all duration-300 origin-top md:w-[217px] max-h-60 overflow-y-auto">
+            {statusOptions.map((status) => (
+              <li
+                key={status}
+                onClick={() => handleSelect(status)}
+                className="flex items-center gap-3 px-4 hover:bg-gray-100 cursor-pointer text-lg w-full h-12 md:h-[48px]"
+              >
+                {value === status ? (
+                  <Image
+                    src="/assets/image/check.svg"
+                    alt="check"
+                    width={22}
+                    height={22}
+                  />
+                ) : (
+                  <div className="w-[22px] h-[22px]" />
+                )}
+
+                <div className="inline-flex items-center gap-2 px-3 py-2 bg-[#F1E5FF] rounded-full">
+                  <span className="w-2 h-2 rounded-full bg-[#5534DA]" />
+                  <span className="text-[#5534DA]">{status}</span>
+                </div>
+              </li>
+            ))}
+          </ul>
+        )}
+      </div>
+    </div>
+  )
+}
path: src/components/dropdown/StatusDropdown.tsx
user: login: Parkchanyoung0710
body: 네 반영하겠습니다

created_at: 2025-04-28T03:15:11Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,135 @@
+import { useEffect, useRef, useState, useCallback } from 'react'
+import Image from 'next/image'
+
+interface User {
+  id: number
+  name: string
+  badgeColor: string
+}
+
+interface UserDropdownProps {
+  users: User[]
+  selectedUser: User
+  onChange: (user: User) => void
+}
+
+export default function UserDropdown({
+  users,
+  selectedUser,
+  onChange,
+}: UserDropdownProps) {
+  const [isOpen, setIsOpen] = useState(false)
+  const dropdownRef = useRef<HTMLDivElement>(null)
+
+  const handleSelect = useCallback(
+    (user: User) => {
+      onChange(user)
+      setIsOpen(false)
+    },
+    [onChange]
+  )
+
+  const getInitial = useCallback((name: string) => {
+    if (!name) return 'U'
+    const firstChar = name.trim().charAt(0)
+    const isEnglish = /^[A-Za-z]$/.test(firstChar)
+    // API 서버에서 이름을 가져와서 사용해야함
+    const hangulToRoman: { [key: string]: string } = {
+      하: 'H',
+      배: 'B',
+      김: 'K',
+      이: 'L',
+      박: 'P',
+      최: 'C',
+      정: 'J',
+      강: 'K',
+      조: 'J',
+      윤: 'Y',
+      장: 'J',
+    }
+
+    return isEnglish ? firstChar.toUpperCase() : hangulToRoman[firstChar] || 'U'
+  }, [])
+
+  useEffect(() => {
+    const handleKeyDown = (event: KeyboardEvent) => {
+      if (event.key === 'Escape' && isOpen) {
+        setIsOpen(false)
+
+        // 포커스 해제
+        const activeElement = document.activeElement as HTMLElement
+        activeElement?.blur()
+      }
+    }
+
+    document.addEventListener('keydown', handleKeyDown)
+
+    return () => {
+      document.removeEventListener('keydown', handleKeyDown)
+    }
+  }, [isOpen])
+
+  return (
+    <div className="flex items-center justify-center min-h-screen p-4">
+      <div
+        ref={dropdownRef}
+        className="relative inline-block w-full max-w-sm min-w-[200px] md:w-[217px]"
+      >
+        <button
+          onClick={() => setIsOpen((prev) => !prev)}
+          className={`w-full h-12 md:w-[217px] md:h-[48px] flex justify-between items-center px-4 py-3 rounded-md text-lg bg-white focus:outline-none 
+    ${isOpen ? 'border-[#5534DA]' : 'border-[#D9D9D9]'} border`}
+        >
+          <div className="flex items-center gap-3">
+            <div
+              className={`w-8 h-8 flex items-center justify-center rounded-full text-white text-sm ${selectedUser.badgeColor}`}
+            >
+              {getInitial(selectedUser.name)}
+            </div>
+            <span className="truncate">{selectedUser.name}</span>
path: src/components/dropdown/UserDropdown.tsx
user: login: Parkchanyoung0710
body: 넵
created_at: 2025-04-28T03:21:48Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,135 @@
+import { useEffect, useRef, useState, useCallback } from 'react'
+import Image from 'next/image'
+
+interface User {
+  id: number
+  name: string
+  badgeColor: string
+}
+
+interface UserDropdownProps {
+  users: User[]
+  selectedUser: User
+  onChange: (user: User) => void
+}
+
+export default function UserDropdown({
+  users,
+  selectedUser,
+  onChange,
+}: UserDropdownProps) {
+  const [isOpen, setIsOpen] = useState(false)
+  const dropdownRef = useRef<HTMLDivElement>(null)
+
+  const handleSelect = useCallback(
+    (user: User) => {
+      onChange(user)
+      setIsOpen(false)
+    },
+    [onChange]
+  )
+
+  const getInitial = useCallback((name: string) => {
+    if (!name) return 'U'
+    const firstChar = name.trim().charAt(0)
+    const isEnglish = /^[A-Za-z]$/.test(firstChar)
+    // API 서버에서 이름을 가져와서 사용해야함
+    const hangulToRoman: { [key: string]: string } = {
+      하: 'H',
+      배: 'B',
+      김: 'K',
+      이: 'L',
+      박: 'P',
+      최: 'C',
+      정: 'J',
+      강: 'K',
+      조: 'J',
+      윤: 'Y',
+      장: 'J',
+    }
+
+    return isEnglish ? firstChar.toUpperCase() : hangulToRoman[firstChar] || 'U'
+  }, [])
+
+  useEffect(() => {
+    const handleKeyDown = (event: KeyboardEvent) => {
+      if (event.key === 'Escape' && isOpen) {
+        setIsOpen(false)
+
+        // 포커스 해제
+        const activeElement = document.activeElement as HTMLElement
+        activeElement?.blur()
+      }
+    }
+
+    document.addEventListener('keydown', handleKeyDown)
+
+    return () => {
+      document.removeEventListener('keydown', handleKeyDown)
+    }
+  }, [isOpen])
+
+  return (
+    <div className="flex items-center justify-center min-h-screen p-4">
+      <div
+        ref={dropdownRef}
+        className="relative inline-block w-full max-w-sm min-w-[200px] md:w-[217px]"
+      >
+        <button
+          onClick={() => setIsOpen((prev) => !prev)}
+          className={`w-full h-12 md:w-[217px] md:h-[48px] flex justify-between items-center px-4 py-3 rounded-md text-lg bg-white focus:outline-none 
+    ${isOpen ? 'border-[#5534DA]' : 'border-[#D9D9D9]'} border`}
+        >
+          <div className="flex items-center gap-3">
+            <div
+              className={`w-8 h-8 flex items-center justify-center rounded-full text-white text-sm ${selectedUser.badgeColor}`}
+            >
+              {getInitial(selectedUser.name)}
+            </div>
+            <span className="truncate">{selectedUser.name}</span>
+          </div>
+          <Image
+            src="/assets/image/arrow-down.svg"
+            alt="Arrow Down"
+            width={24}
+            height={24}
+            className={`transition-transform duration-300 ${
+              isOpen ? 'rotate-180' : ''
+            }`}
+          />
+        </button>
+
+        {isOpen && (
+          <ul className="absolute z-10 mt-2 w-full border border-[#D9D9D9] bg-white rounded-md transition-all duration-300 origin-top md:w-[217px] max-h-60 overflow-y-auto">
+            {users.map((user) => (
+              <li
+                key={user.id}
+                onClick={() => handleSelect(user)}
+                className="flex items-center gap-3 px-4 hover:bg-gray-100 cursor-pointer text-lg w-full h-12 md:h-[48px]"
+              >
+                {selectedUser.id === user.id ? (
+                  <Image
+                    src="/assets/image/check.svg"
+                    alt="check"
+                    width={22}
+                    height={22}
+                  />
+                ) : (
+                  <div className="w-[22px] h-[22px]" />
+                )}
+                <div className="flex items-center gap-3 flex-1">
+                  <div
+                    className={`w-8 h-8 flex items-center justify-center rounded-full text-white text-sm ${user.badgeColor}`}
path: src/components/dropdown/UserDropdown.tsx
user: login: Parkchanyoung0710
body: 뱃지 사이즈 수정했습니다
created_at: 2025-04-28T04:24:24Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,101 @@
+import { useEffect, useRef, useState } from 'react'
+import Image from 'next/image'
+
+// API에서 상태 데이터를 받아와서 사용해야함
+type Status = 'To Do' | 'On Progress' | 'Done'
+
+interface StatusDropdownProps {
+  value: Status
+  onChange: (status: Status) => void
+}
+
+const statusOptions: Status[] = ['To Do', 'On Progress', 'Done']
+
+export default function StatusDropdown({
+  value,
+  onChange,
+}: StatusDropdownProps) {
+  const [isOpen, setIsOpen] = useState(false)
+  const dropdownRef = useRef<HTMLDivElement>(null)
+
+  const handleSelect = (status: Status) => {
+    onChange(status)
+    setIsOpen(false)
+  }
+
+  useEffect(() => {
+    const handleKeyDown = (event: KeyboardEvent) => {
+      if (event.key === 'Escape' && isOpen) {
+        setIsOpen(false)
+
+        // 포커스 해제
+        const activeElement = document.activeElement as HTMLElement
+        activeElement?.blur()
+      }
+    }
+
+    document.addEventListener('keydown', handleKeyDown)
+
+    return () => {
+      document.removeEventListener('keydown', handleKeyDown)
+    }
+  }, [isOpen])
+  return (
+    <div className="flex items-center justify-center min-h-screen">
+      <div
+        ref={dropdownRef}
+        className="relative inline-block w-full max-w-sm min-w-[200px] md:w-[217px]"
+      >
+        <button
+          onClick={() => setIsOpen((prev) => !prev)}
+          className={`w-full h-12 md:w-[217px] md:h-[48px] flex justify-between items-center px-4 py-3 rounded-md text-lg bg-white focus:outline-none 
path: src/components/dropdown/StatusDropdown.tsx
user: login: Parkchanyoung0710
body: 8px로 동일하게 적용했습니다
created_at: 2025-04-28T04:24:41Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,101 @@
+import { useEffect, useRef, useState } from 'react'
+import Image from 'next/image'
+
+// API에서 상태 데이터를 받아와서 사용해야함
+type Status = 'To Do' | 'On Progress' | 'Done'
+
+interface StatusDropdownProps {
+  value: Status
+  onChange: (status: Status) => void
+}
+
+const statusOptions: Status[] = ['To Do', 'On Progress', 'Done']
+
+export default function StatusDropdown({
+  value,
+  onChange,
+}: StatusDropdownProps) {
+  const [isOpen, setIsOpen] = useState(false)
+  const dropdownRef = useRef<HTMLDivElement>(null)
+
+  const handleSelect = (status: Status) => {
+    onChange(status)
+    setIsOpen(false)
+  }
+
+  useEffect(() => {
+    const handleKeyDown = (event: KeyboardEvent) => {
+      if (event.key === 'Escape' && isOpen) {
+        setIsOpen(false)
+
+        // 포커스 해제
+        const activeElement = document.activeElement as HTMLElement
+        activeElement?.blur()
+      }
+    }
+
+    document.addEventListener('keydown', handleKeyDown)
+
+    return () => {
+      document.removeEventListener('keydown', handleKeyDown)
+    }
+  }, [isOpen])
+  return (
+    <div className="flex items-center justify-center min-h-screen">
+      <div
+        ref={dropdownRef}
+        className="relative inline-block w-full max-w-sm min-w-[200px] md:w-[217px]"
+      >
+        <button
+          onClick={() => setIsOpen((prev) => !prev)}
+          className={`w-full h-12 md:w-[217px] md:h-[48px] flex justify-between items-center px-4 py-3 rounded-md text-lg bg-white focus:outline-none 
+    ${isOpen ? 'border-[#5534DA]' : 'border-[#D9D9D9]'} border`}
+        >
+          <div className="flex items-center gap-3">
+            <div className="inline-flex items-center gap-2 px-3 py-2 bg-[#F1E5FF] rounded-full">
path: src/components/dropdown/StatusDropdown.tsx
user: login: Parkchanyoung0710
body: #5534DA 8% 투명도 배경으로 바꿨습니다
created_at: 2025-04-28T04:28:43Z
author_association: COLLABORATOR

PR comments:
user: login: ramong26
created_at: 2025-04-27T15:37:36Z
body: 이것도 확인 부탁드립니다.,! 화면 줄이면 이상해지네요

![화면 줄이면 이상함](https://github.com/user-attachments/assets/98f85f6f-37d2-4b49-a774-b111db2e1f7e)

author_association: COLLABORATOR
user: login: Parkchanyoung0710
created_at: 2025-04-28T05:01:45Z
body: > 이것도 확인 부탁드립니다.,! 화면 줄이면 이상해지네요
> 
> ![화면 줄이면 이상함](https://private-user-images.githubusercontent.com/192767726/437969268-98f85f6f-37d2-4b49-a774-b111db2e1f7e.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDU4MTY3NzEsIm5iZiI6MTc0NTgxNjQ3MSwicGF0aCI6Ii8xOTI3Njc3MjYvNDM3OTY5MjY4LTk4Zjg1ZjZmLTM3ZDItNGI0OS1hNzc0LWIxMTFkYjJlMWY3ZS5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUwNDI4JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MDQyOFQwNTAxMTFaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT04YjMzMDM0NGM4MjBkOWFmNDI4MGM1ZTQ3YTFjMTBhMWZkYzJlNTlhZTRjMjAxNGJmOWQ3NGIzNTliYzEwMmY1JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.yaZREm29AsgxWBfhjpqGOc3b61nHxgKDMNKv0RG1_Sc)

반응형은 적용안했습니다
author_association: COLLABORATOR