Repository: 100-hours-a-week/15-Leafresh-FE
Pull request #56: FE: [feat] TimePicker Implementation
Author: login: sunsetdust
Labels: name: feat
description: 기능 구현 이슈
name: FE
description: 프론트엔드 이슈
Created: 2025-05-07T04:42:45Z | Merged: 2025-05-10T09:01:45Z

# 🍀 TimePicker 영상

https://github.com/user-attachments/assets/15860878-ab42-4d1e-b3f8-f1984bd7c276

---

### 관련 이슈

Relates to #17 
Closes #52 

## 주요 변경사항

### 1. 시간을 선택하는 Dropdown과 범위를 선택하는 TimePicker를 분할하여 개발하였습니다.
### 2. 드롭다운 형식으로 시간과 분을 따로 선택하도록 했습니다.
### 3. 타 컴포넌트와 마찬가지로 border-bottom transition을 적용했습니다.
### 4. 사용 방식
```
const [start, setStart] = useState('00:00')
const [end, setEnd] = useState('23:59')
<TimePicker
    label="인증 가능 시간 *"
    startValue={start}
    endValue={end}
    onChangeStart={setStart}
    onChangeEnd={setEnd}
/>
```

## 수정 사항
1. useOutClick 커스텀 훅 대신 자체 훅을 useEffect로 구현하였습니다.

### PR간 오류 혹은 질문은 댓글로 남겨주세요!


Linked issue #52: FE: [feat] TimePicker Implementation
### 🌱 컴포넌트 사진

![Image](https://github.com/user-attachments/assets/5e5b3fd3-699c-4607-afb1-913b92103a9c)

---

### 🌱 컴포넌트 설명

- [ ] 시간(시, 분)을 드롭다운으로 선택
- [ ] default 시각 적용
---

Diff:
diff --git a/package.json b/package.json
index 2f66b6cb..3f335f73 100644
--- a/package.json
+++ b/package.json
@@ -48,6 +48,7 @@
     "eslint-plugin-unused-imports": "^4.1.4",
     "husky": "^9.1.7",
     "jsdom": "^26.1.0",
+    "lint-staged": "^15.5.2",
     "prettier": "3.5.3",
     "typescript": "^5",
     "vite-tsconfig-paths": "^5.1.4",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1a0c8a4e..a6f98947 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -114,6 +114,9 @@ importers:
       jsdom:
         specifier: ^26.1.0
         version: 26.1.0
+      lint-staged:
+        specifier: ^15.5.2
+        version: 15.5.2
       prettier:
         specifier: 3.5.3
         version: 3.5.3
@@ -1344,10 +1347,18 @@ packages:
   ajv@6.12.6:
     resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
 
+  ansi-escapes@7.0.0:
+    resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==}
+    engines: {node: '>=18'}
+
   ansi-regex@5.0.1:
     resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
     engines: {node: '>=8'}
 
+  ansi-regex@6.1.0:
+    resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==}
+    engines: {node: '>=12'}
+
   ansi-styles@4.3.0:
     resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
     engines: {node: '>=8'}
@@ -1356,6 +1367,10 @@ packages:
     resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
     engines: {node: '>=10'}
 
+  ansi-styles@6.2.1:
+    resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
+    engines: {node: '>=12'}
+
   argparse@2.0.1:
     resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
 
@@ -1478,10 +1493,22 @@ packages:
     resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
     engines: {node: '>=10'}
 
+  chalk@5.4.1:
+    resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==}
+    engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
+
   check-error@2.1.1:
     resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
     engines: {node: '>= 16'}
 
+  cli-cursor@5.0.0:
+    resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
+    engines: {node: '>=18'}
+
+  cli-truncate@4.0.0:
+    resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==}
+    engines: {node: '>=18'}
+
   client-only@0.0.1:
     resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
 
@@ -1499,6 +1526,13 @@ packages:
     resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
     engines: {node: '>=12.5.0'}
 
+  colorette@2.0.20:
+    resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
+
+  commander@13.1.0:
+    resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
+    engines: {node: '>=18'}
+
   concat-map@0.0.1:
     resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
 
@@ -1615,6 +1649,9 @@ packages:
   embla-carousel@8.6.0:
     resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==}
 
+  emoji-regex@10.4.0:
+    resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==}
+
   emoji-regex@9.2.2:
     resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
 
@@ -1622,6 +1659,10 @@ packages:
     resolution: {integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==}
     engines: {node: '>=0.12'}
 
+  environment@1.1.0:
+    resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
+    engines: {node: '>=18'}
+
   error-ex@1.3.2:
     resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
 
@@ -1829,6 +1870,13 @@ packages:
     resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
     engines: {node: '>=0.10.0'}
 
+  eventemitter3@5.0.1:
+    resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
+
+  execa@8.0.1:
+    resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
+    engines: {node: '>=16.17'}
+
   expect-type@1.2.1:
     resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==}
     engines: {node: '>=12.0.0'}
@@ -1912,6 +1960,10 @@ packages:
     resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
     engines: {node: '>=6.9.0'}
 
+  get-east-asian-width@1.3.0:
+    resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==}
+    engines: {node: '>=18'}
+
   get-intrinsic@1.3.0:
     resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
     engines: {node: '>= 0.4'}
@@ -1920,6 +1972,10 @@ packages:
     resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
     engines: {node: '>= 0.4'}
 
+  get-stream@8.0.1:
+    resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==}
+    engines: {node: '>=16'}
+
   get-symbol-description@1.1.0:
     resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
     engines: {node: '>= 0.4'}
@@ -1999,6 +2055,10 @@ packages:
     resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
     engines: {node: '>= 14'}
 
+  human-signals@5.0.0:
+    resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
+    engines: {node: '>=16.17.0'}
+
   husky@9.1.7:
     resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
     engines: {node: '>=18'}
@@ -2073,6 +2133,14 @@ packages:
     resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==}
     engines: {node: '>= 0.4'}
 
+  is-fullwidth-code-point@4.0.0:
+    resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==}
+    engines: {node: '>=12'}
+
+  is-fullwidth-code-point@5.0.0:
+    resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==}
+    engines: {node: '>=18'}
+
   is-generator-function@1.1.0:
     resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==}
     engines: {node: '>= 0.4'}
@@ -2108,6 +2176,10 @@ packages:
     resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==}
     engines: {node: '>= 0.4'}
 
+  is-stream@3.0.0:
+    resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
+    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+
   is-string@1.1.1:
     resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
     engines: {node: '>= 0.4'}
@@ -2202,9 +2274,22 @@ packages:
     resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
     engines: {node: '>= 0.8.0'}
 
+  lilconfig@3.1.3:
+    resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
+    engines: {node: '>=14'}
+
   lines-and-columns@1.2.4:
     resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
 
+  lint-staged@15.5.2:
+    resolution: {integrity: sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==}
+    engines: {node: '>=18.12.0'}
+    hasBin: true
+
+  listr2@8.3.3:
+    resolution: {integrity: sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==}
+    engines: {node: '>=18.0.0'}
+
   locate-path@6.0.0:
     resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
     engines: {node: '>=10'}
@@ -2212,6 +2297,10 @@ packages:
   lodash.merge@4.6.2:
     resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
 
+  log-update@6.1.0:
+    resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==}
+    engines: {node: '>=18'}
+
   loose-envify@1.4.0:
     resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
     hasBin: true
@@ -2241,6 +2330,9 @@ packages:
     resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
     engines: {node: '>= 0.4'}
 
+  merge-stream@2.0.0:
+    resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
+
   merge2@1.4.1:
     resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
     engines: {node: '>= 8'}
@@ -2249,6 +2341,14 @@ packages:
     resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
     engines: {node: '>=8.6'}
 
+  mimic-fn@4.0.0:
+    resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
+    engines: {node: '>=12'}
+
+  mimic-function@5.0.1:
+    resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
+    engines: {node: '>=18'}
+
   minimatch@3.1.2:
     resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
 
@@ -2299,6 +2399,10 @@ packages:
   node-releases@2.0.19:
     resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
 
+  npm-run-path@5.3.0:
+    resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==}
+    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+
   nwsapi@2.2.20:
     resolution: {integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==}
 
@@ -2334,6 +2438,14 @@ packages:
     resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
     engines: {node: '>= 0.4'}
 
+  onetime@6.0.0:
+    resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
+    engines: {node: '>=12'}
+
+  onetime@7.0.0:
+    resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
+    engines: {node: '>=18'}
+
   optionator@0.9.4:
     resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
     engines: {node: '>= 0.8.0'}
@@ -2369,6 +2481,10 @@ packages:
     resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
     engines: {node: '>=8'}
 
+  path-key@4.0.0:
+    resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
+    engines: {node: '>=12'}
+
   path-parse@1.0.7:
     resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
 
@@ -2397,6 +2513,11 @@ packages:
     resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
     engines: {node: '>=12'}
 
+  pidtree@0.6.0:
+    resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==}
+    engines: {node: '>=0.10'}
+    hasBin: true
+
   possible-typed-array-names@1.1.0:
     resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
     engines: {node: '>= 0.4'}
@@ -2494,10 +2615,17 @@ packages:
     resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==}
     hasBin: true
 
+  restore-cursor@5.1.0:
+    resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
+    engines: {node: '>=18'}
+
   reusify@1.1.0:
     resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
     engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
 
+  rfdc@1.4.1:
+    resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
+
   rollup@4.40.1:
     resolution: {integrity: sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==}
     engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@@ -2583,9 +2711,21 @@ packages:
   siginfo@2.0.0:
     resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
 
+  signal-exit@4.1.0:
+    resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
+    engines: {node: '>=14'}
+
   simple-swizzle@0.2.2:
     resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
 
+  slice-ansi@5.0.0:
+    resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==}
+    engines: {node: '>=12'}
+
+  slice-ansi@7.1.0:
+    resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==}
+    engines: {node: '>=18'}
+
   source-map-js@1.2.1:
     resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
     engines: {node: '>=0.10.0'}
@@ -2607,6 +2747,14 @@ packages:
     resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
     engines: {node: '>=10.0.0'}
 
+  string-argv@0.3.2:
+    resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
+    engines: {node: '>=0.6.19'}
+
+  string-width@7.2.0:
+    resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
+    engines: {node: '>=18'}
+
   string.prototype.includes@2.0.1:
     resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==}
     engines: {node: '>= 0.4'}
@@ -2630,10 +2778,18 @@ packages:
     resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
     engines: {node: '>= 0.4'}
 
+  strip-ansi@7.1.0:
+    resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
+    engines: {node: '>=12'}
+
   strip-bom@3.0.0:
     resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
     engines: {node: '>=4'}
 
+  strip-final-newline@3.0.0:
+    resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
+    engines: {node: '>=12'}
+
   strip-json-comments@3.1.1:
     resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
     engines: {node: '>=8'}
@@ -2910,6 +3066,10 @@ packages:
     resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
     engines: {node: '>=0.10.0'}
 
+  wrap-ansi@9.0.0:
+    resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==}
+    engines: {node: '>=18'}
+
   ws@8.18.1:
     resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==}
     engines: {node: '>=10.0.0'}
@@ -4422,14 +4582,22 @@ snapshots:
       json-schema-traverse: 0.4.1
       uri-js: 4.4.1
 
+  ansi-escapes@7.0.0:
+    dependencies:
+      environment: 1.1.0
+
   ansi-regex@5.0.1: {}
 
+  ansi-regex@6.1.0: {}
+
   ansi-styles@4.3.0:
     dependencies:
       color-convert: 2.0.1
 
   ansi-styles@5.2.0: {}
 
+  ansi-styles@6.2.1: {}
+
   argparse@2.0.1: {}
 
   aria-query@5.3.0:
@@ -4585,8 +4753,19 @@ snapshots:
       ansi-styles: 4.3.0
       supports-color: 7.2.0
 
+  chalk@5.4.1: {}
+
   check-error@2.1.1: {}
 
+  cli-cursor@5.0.0:
+    dependencies:
+      restore-cursor: 5.1.0
+
+  cli-truncate@4.0.0:
+    dependencies:
+      slice-ansi: 5.0.0
+      string-width: 7.2.0
+
   client-only@0.0.1: {}
 
   color-convert@2.0.1:
@@ -4607,6 +4786,10 @@ snapshots:
       color-string: 1.9.1
     optional: true
 
+  colorette@2.0.20: {}
+
+  commander@13.1.0: {}
+
   concat-map@0.0.1: {}
 
   convert-source-map@1.9.0: {}
@@ -4718,10 +4901,14 @@ snapshots:
 
   embla-carousel@8.6.0: {}
 
+  emoji-regex@10.4.0: {}
+
   emoji-regex@9.2.2: {}
 
   entities@6.0.0: {}
 
+  environment@1.1.0: {}
+
   error-ex@1.3.2:
     dependencies:
       is-arrayish: 0.2.1
@@ -5082,6 +5269,20 @@ snapshots:
 
   esutils@2.0.3: {}
 
+  eventemitter3@5.0.1: {}
+
+  execa@8.0.1:
+    dependencies:
+      cross-spawn: 7.0.6
+      get-stream: 8.0.1
+      human-signals: 5.0.0
+      is-stream: 3.0.0
+      merge-stream: 2.0.0
+      npm-run-path: 5.3.0
+      onetime: 6.0.0
+      signal-exit: 4.1.0
+      strip-final-newline: 3.0.0
+
   expect-type@1.2.1: {}
 
   fast-deep-equal@3.1.3: {}
@@ -5162,6 +5363,8 @@ snapshots:
 
   gensync@1.0.0-beta.2: {}
 
+  get-east-asian-width@1.3.0: {}
+
   get-intrinsic@1.3.0:
     dependencies:
       call-bind-apply-helpers: 1.0.2
@@ -5180,6 +5383,8 @@ snapshots:
       dunder-proto: 1.0.1
       es-object-atoms: 1.1.1
 
+  get-stream@8.0.1: {}
+
   get-symbol-description@1.1.0:
     dependencies:
       call-bound: 1.0.4
@@ -5257,6 +5462,8 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  human-signals@5.0.0: {}
+
   husky@9.1.7: {}
 
   iconv-lite@0.6.3:
@@ -5333,6 +5540,12 @@ snapshots:
     dependencies:
       call-bound: 1.0.4
 
+  is-fullwidth-code-point@4.0.0: {}
+
+  is-fullwidth-code-point@5.0.0:
+    dependencies:
+      get-east-asian-width: 1.3.0
+
   is-generator-function@1.1.0:
     dependencies:
       call-bound: 1.0.4
@@ -5368,6 +5581,8 @@ snapshots:
     dependencies:
       call-bound: 1.0.4
 
+  is-stream@3.0.0: {}
+
   is-string@1.1.1:
     dependencies:
       call-bound: 1.0.4
@@ -5478,14 +5693,48 @@ snapshots:
       prelude-ls: 1.2.1
       type-check: 0.4.0
 
+  lilconfig@3.1.3: {}
+
   lines-and-columns@1.2.4: {}
 
+  lint-staged@15.5.2:
+    dependencies:
+      chalk: 5.4.1
+      commander: 13.1.0
+      debug: 4.4.0
+      execa: 8.0.1
+      lilconfig: 3.1.3
+      listr2: 8.3.3
+      micromatch: 4.0.8
+      pidtree: 0.6.0
+      string-argv: 0.3.2
+      yaml: 2.7.1
+    transitivePeerDependencies:
+      - supports-color
+
+  listr2@8.3.3:
+    dependencies:
+      cli-truncate: 4.0.0
+      colorette: 2.0.20
+      eventemitter3: 5.0.1
+      log-update: 6.1.0
+      rfdc: 1.4.1
+      wrap-ansi: 9.0.0
+
   locate-path@6.0.0:
     dependencies:
       p-locate: 5.0.0
 
   lodash.merge@4.6.2: {}
 
+  log-update@6.1.0:
+    dependencies:
+      ansi-escapes: 7.0.0
+      cli-cursor: 5.0.0
+      slice-ansi: 7.1.0
+      strip-ansi: 7.1.0
+      wrap-ansi: 9.0.0
+
   loose-envify@1.4.0:
     dependencies:
       js-tokens: 4.0.0
@@ -5510,6 +5759,8 @@ snapshots:
 
   math-intrinsics@1.1.0: {}
 
+  merge-stream@2.0.0: {}
+
   merge2@1.4.1: {}
 
   micromatch@4.0.8:
@@ -5517,6 +5768,10 @@ snapshots:
       braces: 3.0.3
       picomatch: 2.3.1
 
+  mimic-fn@4.0.0: {}
+
+  mimic-function@5.0.1: {}
+
   minimatch@3.1.2:
     dependencies:
       brace-expansion: 1.1.11
@@ -5562,6 +5817,10 @@ snapshots:
 
   node-releases@2.0.19: {}
 
+  npm-run-path@5.3.0:
+    dependencies:
+      path-key: 4.0.0
+
   nwsapi@2.2.20: {}
 
   object-assign@4.1.1: {}
@@ -5606,6 +5865,14 @@ snapshots:
       define-properties: 1.2.1
       es-object-atoms: 1.1.1
 
+  onetime@6.0.0:
+    dependencies:
+      mimic-fn: 4.0.0
+
+  onetime@7.0.0:
+    dependencies:
+      mimic-function: 5.0.1
+
   optionator@0.9.4:
     dependencies:
       deep-is: 0.1.4
@@ -5648,6 +5915,8 @@ snapshots:
 
   path-key@3.1.1: {}
 
+  path-key@4.0.0: {}
+
   path-parse@1.0.7: {}
 
   path-type@4.0.0: {}
@@ -5664,6 +5933,8 @@ snapshots:
 
   picomatch@4.0.2: {}
 
+  pidtree@0.6.0: {}
+
   possible-typed-array-names@1.1.0: {}
 
   postcss@8.4.31:
@@ -5763,8 +6034,15 @@ snapshots:
       path-parse: 1.0.7
       supports-preserve-symlinks-flag: 1.0.0
 
+  restore-cursor@5.1.0:
+    dependencies:
+      onetime: 7.0.0
+      signal-exit: 4.1.0
+
   reusify@1.1.0: {}
 
+  rfdc@1.4.1: {}
+
   rollup@4.40.1:
     dependencies:
       '@types/estree': 1.0.7
@@ -5914,11 +6192,23 @@ snapshots:
 
   siginfo@2.0.0: {}
 
+  signal-exit@4.1.0: {}
+
   simple-swizzle@0.2.2:
     dependencies:
       is-arrayish: 0.3.2
     optional: true
 
+  slice-ansi@5.0.0:
+    dependencies:
+      ansi-styles: 6.2.1
+      is-fullwidth-code-point: 4.0.0
+
+  slice-ansi@7.1.0:
+    dependencies:
+      ansi-styles: 6.2.1
+      is-fullwidth-code-point: 5.0.0
+
   source-map-js@1.2.1: {}
 
   source-map@0.5.7: {}
@@ -5931,6 +6221,14 @@ snapshots:
 
   streamsearch@1.1.0: {}
 
+  string-argv@0.3.2: {}
+
+  string-width@7.2.0:
+    dependencies:
+      emoji-regex: 10.4.0
+      get-east-asian-width: 1.3.0
+      strip-ansi: 7.1.0
+
   string.prototype.includes@2.0.1:
     dependencies:
       call-bind: 1.0.8
@@ -5981,8 +6279,14 @@ snapshots:
       define-properties: 1.2.1
       es-object-atoms: 1.1.1
 
+  strip-ansi@7.1.0:
+    dependencies:
+      ansi-regex: 6.1.0
+
   strip-bom@3.0.0: {}
 
+  strip-final-newline@3.0.0: {}
+
   strip-json-comments@3.1.1: {}
 
   styled-jsx@5.1.6(@babel/core@7.26.10)(react@19.1.0):
@@ -6292,6 +6596,12 @@ snapshots:
 
   word-wrap@1.2.5: {}
 
+  wrap-ansi@9.0.0:
+    dependencies:
+      ansi-styles: 6.2.1
+      string-width: 7.2.0
+      strip-ansi: 7.1.0
+
   ws@8.18.1: {}
 
   xml-name-validator@5.0.0: {}
@@ -6302,8 +6612,7 @@ snapshots:
 
   yaml@1.10.2: {}
 
-  yaml@2.7.1:
-    optional: true
+  yaml@2.7.1: {}
 
   yocto-queue@0.1.0: {}
 
diff --git a/src/app/(with-header)/challenge/page.tsx b/src/app/(with-header)/challenge/page.tsx
index f92f6e7a..06f43d93 100644
--- a/src/app/(with-header)/challenge/page.tsx
+++ b/src/app/(with-header)/challenge/page.tsx
@@ -3,50 +3,33 @@
 
 import { useState } from 'react'
 import styled from '@emotion/styled'
-
-import SwitchTap from '@shared/components/switchtap/SwitchTap'
+import TimePicker from '@shared/components/timepicker/TimePicker'
 
 export default function Page() {
-  const [tab, setTab] = useState(0)
+  const [start, setStart] = useState('00:00')
+  const [end, setEnd] = useState('23:59')
 
   return (
-    <Wrapper>
-      <SwitchTap tabs={['카메라', '카메라2', '인증 방법']} currentIndex={tab} onChange={setTab} />
-
-      <ContentArea>
-        {tab === 0 ? (
-          <Placeholder>📷 카메라 화면을 여기에 렌더링</Placeholder>
-        ) : (
-          <Placeholder>🔐 인증 방법 설명을 여기에 렌더링</Placeholder>
-        )}
-      </ContentArea>
-    </Wrapper>
+    <Container>
+      <TimePicker
+        label='인증 가능 시간 *'
+        startValue={start}
+        endValue={end}
+        onChangeStart={setStart}
+        onChangeEnd={setEnd}
+      />
+      <>
+        {start} ~ {end}
+      </>
+    </Container>
   )
 }
 
-const Wrapper = styled.div`
-  width: 100%;
-  display: flex;
-  flex-direction: column;
-  align-items: center; /* 가운데 정렬 */
+const Container = styled.div`
   padding: 24px;
-  gap: 24px; /* 컴포넌트 간 여백 */
-`
-
-const ContentArea = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  width: 100%;
-  max-width: 480px; /* 반응형 최대 너비 */
-  min-height: 240px; /* 기본 높이 */
-  padding: 16px;
-  border: 1px solid #eee;
-  border-radius: 8px;
-  background: #fafafa;
-`
-
-const Placeholder = styled.div`
-  font-size: 1rem;
-  color: #666;
+  color: black;
 `
+const Output = styled.div`
+  margin-top: 16px;
+  font-size: 14px;
+`
\ No newline at end of file
diff --git a/src/shared/components/timepicker/TimeDropdown.tsx b/src/shared/components/timepicker/TimeDropdown.tsx
new file mode 100644
index 00000000..1ebee7db
--- /dev/null
+++ b/src/shared/components/timepicker/TimeDropdown.tsx
@@ -0,0 +1,189 @@
+'use client'
+
+import { useRef, useState, useEffect } from 'react'
+import styled from '@emotion/styled'
+import { theme } from '@shared/styles/emotion/theme'
+
+export interface TimeDropdownProps {
+  value: string
+  isOpen: boolean
+  onConfirm: (newValue: string) => void
+  onOpenChange: (isOpen: boolean) => void
+  onCancel?: () => void
+}
+
+export default function TimeDropdown({ value, isOpen, onConfirm, onCancel, onOpenChange }: TimeDropdownProps) {
+  const [initH, initM] = value.split(':').map(s => parseInt(s, 10))
+  const [hour, setHour] = useState(isNaN(initH) ? 0 : initH)
+  const [minute, setMinute] = useState(isNaN(initM) ? 0 : initM)
+
+  const wrapperRef = useRef<HTMLDivElement>(null)
+  const dropdownRef = useRef<HTMLDivElement>(null)
+
+  const handleConfirm = () => {
+    const hh = String(hour).padStart(2, '0')
+    const mm = String(minute).padStart(2, '0')
+    onConfirm(`${hh}:${mm}`)
+    onOpenChange(false)
+  }
+
+  const handleClose = () => {
+    onOpenChange(false)
+    onCancel?.()
+
+    //취소했을 때 반영되지 않게끔 변경
+    const [resetH, resetM] = value.split(':').map(s => parseInt(s, 10))
+    setHour(!isNaN(resetH) ? resetH : 0)
+    setMinute(!isNaN(resetM) ? resetM : 0)
+  }
+
+  // 외부 클릭 감지 핸들러
+  useEffect(() => {
+    if (!isOpen) return
+
+    const handleClickOutside = (event: MouseEvent) => {
+      // 드롭다운이 열려있을 때만 처리
+      if (!wrapperRef.current || !dropdownRef.current) return
+      const isClickInWrapper = wrapperRef.current.contains(event.target as Node)
+      const isClickInDropdown = dropdownRef.current.contains(event.target as Node)
+
+      // 클릭이 wrapper의 트리거 버튼에서 발생했는지 확인
+      const triggerElement = wrapperRef.current.querySelector('button')
+      const isClickOnTrigger = triggerElement && triggerElement.contains(event.target as Node)
+
+      // 외부 클릭인 경우에만 닫기
+      if (!isClickInWrapper || (isClickInWrapper && !isClickInDropdown && !isClickOnTrigger)) {
+        handleClose()
+      }
+    }
+    document.addEventListener('mousedown', handleClickOutside)
+
+    return () => {
+      //이벤트리스너 제거
+      document.removeEventListener('mousedown', handleClickOutside)
+    }
+  }, [isOpen])
+
+  return (
+    <Wrapper ref={wrapperRef}>
+      <Trigger
+        onClick={() => {
+          onOpenChange(!isOpen)
+        }}
+      >
+        {String(hour).padStart(2, '0')} : {String(minute).padStart(2, '0')}
+      </Trigger>
+      <Dropdown isOpen={isOpen} ref={dropdownRef} onClick={e => e.stopPropagation()}>
+        <Columns>
+          <Column>
+            {Array.from({ length: 24 }, (_, i) => i).map(h => (
+              <Option key={h} selected={h === hour} onClick={() => setHour(h)}>
+                {String(h).padStart(2, '0')}
+              </Option>
+            ))}
+          </Column>
+          <Column style={{ marginLeft: '2px' }}>
+            {Array.from({ length: 60 }, (_, i) => i).map(m => (
+              <Option key={m} selected={m === minute} onClick={() => setMinute(m)}>
+                {String(m).padStart(2, '0')}
+              </Option>
+            ))}
+          </Column>
+        </Columns>
+        <ActionWrapper>
+          <ActButton onClick={handleClose}>Cancel</ActButton>
+          <ActButton primary onClick={handleConfirm}>
+            OK
+          </ActButton>
+        </ActionWrapper>
+      </Dropdown>
+    </Wrapper>
+  )
+}
+
+/* ===== 스타일 ===== */
+const Wrapper = styled.div`
+  position: relative;
+  display: inline-block;
+  width: 100%;
+`
+
+const Trigger = styled.button`
+  width: 100%;
+  appearance: none;
+  background: transparent;
+  border: none;
+  font-size: ${theme.fontSize.xs};
+  font-weight: ${theme.fontWeight.regular};
+  color: ${theme.colors.lfBlack.base};
+  text-align: start;
+  cursor: pointer;
+`
+
+const Dropdown = styled.div<{ isOpen: boolean }>`
+  position: absolute;
+  top: calc(100% + 15px);
+  width: 100%;
+  background: ${theme.colors.lfBackdrop};
+  color: ${theme.colors.lfDarkGray};
+  border-radius: ${theme.radius.sm};
+  overflow: hidden;
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
+  z-index: 1000;
+  display: flex;
+  flex-direction: column;
+
+  opacity: ${({ isOpen }) => (isOpen ? 1 : 0)};
+  transform: ${({ isOpen }) => (isOpen ? 'translateY(0)' : 'translateY(-10px)')};
+  visibility: ${({ isOpen }) => (isOpen ? 'visible' : 'hidden')};
+  pointer-events: ${({ isOpen }) => (isOpen ? 'auto' : 'none')};
+
+  transition:
+    opacity 0.3s ease,
+    transform 0.3s ease,
+    visibility 0s linear ${({ isOpen }) => (isOpen ? '0s' : '0.2s')};
+`
+
+const Columns = styled.div`
+  display: flex;
+`
+const Column = styled.ul`
+  max-height: 200px;
+  overflow-y: auto;
+  border-right: 1px solid ${theme.colors.lfBlue};
+
+  &::-webkit-scrollbar {
+    width: 3px;
+  }
+`
+const Option = styled.li<{ selected: boolean }>`
+  padding: 5px 21px;
+  cursor: pointer;
+  background: ${({ selected }) => (selected ? `${theme.colors.lfGreenMain.hover}` : `${theme.colors.lfWhite.base}`)};
+  color: ${({ selected }) => (selected ? `${theme.colors.lfWhite.base}` : `${theme.colors.lfGray.base}`)};
+  &:hover {
+    background: ${theme.colors.lfGreenInactive.base};
+    color: ${theme.colors.lfDarkGray.base};
+  }
+`
+const ActionWrapper = styled.div`
+  display: flex;
+  justify-content: flex-end;
+  border-top: 1px solid ${theme.colors.lfGreenBorder.base};
+`
+const ActButton = styled.button<{ primary?: boolean }>`
+  flex: 1;
+  padding: 8px 0;
+  background: ${({ primary }) => (primary ? `${theme.colors.lfGreenMain.base}` : 'transparent')};
+  color: ${({ primary }) => (primary ? `${theme.colors.lfWhite.base}` : `${theme.colors.lfBlack.base}`)};
+  border: none;
+
+  font-size: ${theme.fontSize.xs};
+  font-weight: ${theme.fontWeight.bold};
+
+  cursor: pointer;
+  &:hover {
+    background: ${({ primary }) =>
+      primary ? `${theme.colors.lfGreenMain.hover}` : `${theme.colors.lfLightGray.base}`};
+  }
+`
diff --git a/src/shared/components/timepicker/TimePicker.tsx b/src/shared/components/timepicker/TimePicker.tsx
new file mode 100644
index 00000000..fa19596d
--- /dev/null
+++ b/src/shared/components/timepicker/TimePicker.tsx
@@ -0,0 +1,115 @@
+'use client'
+import TimeDropdown from './TimeDropdown'
+
+import { theme } from '@shared/styles/emotion/theme'
+import { useToggle } from '@shared/hooks/useToggle/useToggle'
+import styled from '@emotion/styled'
+
+export interface TimePickerProps {
+  label?: string
+  startValue: string
+  endValue: string
+  onChangeStart: (v: string) => void //기본 시작시간 값
+  onChangeEnd: (v: string) => void //기본 종료시간 값
+}
+
+const TimePicker = ({ label, startValue, endValue, onChangeStart, onChangeEnd }: TimePickerProps) => {
+  const { value: startOpen, toggle: toggleStart, setValue: setStartOpen } = useToggle(false)
+  const { value: endOpen, toggle: toggleEnd, setValue: setEndOpen } = useToggle(false)
+
+  return (
+    <Wrapper>
+      {label && <Title>{label}</Title>}
+      <RangeWrapper isOpen={startOpen || endOpen}>
+        <Panel
+          onClick={() => {
+            setStartOpen(true)
+            setEndOpen(false)
+          }}
+        >
+          <PanelLabel>시작시간</PanelLabel>
+          <TimeDropdown
+            value={startValue}
+            isOpen={startOpen}
+            onConfirm={onChangeStart}
+            onCancel={() => setStartOpen(false)}
+            onOpenChange={() => {
+              toggleStart()
+              setEndOpen(false)
+            }}
+          />
+        </Panel>
+        <Divider />
+        <Panel
+          onClick={() => {
+            setStartOpen(false)
+            setEndOpen(true)
+          }}
+        >
+          <PanelLabel>종료시간</PanelLabel>
+          <TimeDropdown
+            value={endValue}
+            isOpen={endOpen}
+            onConfirm={onChangeEnd}
+            onCancel={() => setEndOpen(false)}
+            onOpenChange={() => {
+              toggleEnd()
+              setStartOpen(false)
+            }}
+          />
+        </Panel>
+      </RangeWrapper>
+    </Wrapper>
+  )
+}
+export default TimePicker
+
+/* ===== 스타일 ===== */
+const Wrapper = styled.div`
+  display: flex;
+  flex-direction: column;
+`
+const Title = styled.h2`
+  font-size: ${theme.fontSize.sm};
+  font-weight: ${theme.fontWeight.semiBold};
+  margin-bottom: 12px;
+  color: ${theme.colors.lfBlack.base};
+`
+const RangeWrapper = styled.div<{ isOpen: boolean }>`
+  position: relative;
+  display: flex;
+  background: #fafafa;
+  border-radius: ${theme.radius.xs};
+  width: 321px;
+  height: 72px;
+
+  &::after {
+    content: '';
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    width: 100%;
+    height: 2px;
+    background-color: ${theme.colors.lfBlack.base};
+
+    transform-origin: center;
+    transform: scaleX(${props => (props.isOpen ? 1 : 0)});
+    transition: transform 0.2s ease;
+  }
+`
+const Panel = styled.div`
+  flex: 1;
+  padding: 12px 16px;
+  cursor: pointer;
+`
+const PanelLabel = styled.div`
+  font-size: ${theme.fontSize.sm};
+  font-weight: ${theme.fontWeight.semiBold};
+  color: ${theme.colors.lfBlack.base};
+  margin-bottom: 4px;
+`
+const Divider = styled.div`
+  width: 0.1px;
+  margin: 5px 0;
+  background: #d3d3d3;
+`


Reviews:
user: login: jihostudy
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-08T05:52:37Z
user: login: sunsetdust
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T08:32:11Z
user: login: sunsetdust
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T08:32:20Z
user: login: sunsetdust
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T11:57:01Z
user: login: jihostudy
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-09T06:36:12Z
user: login: sunsetdust
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T04:31:54Z

Review comments:
diff_hunk: @@ -0,0 +1,177 @@
+'use client'
+
+import { useRef, useState } from 'react'
+import styled from '@emotion/styled'
+import { theme } from '@shared/styles/emotion/theme'
+
+import { useOutsideClick } from '@shared/hooks/useOutsideClick/useOutsideClick'
+
+export interface TimeDropdownProps {
+  value: string
+  open: boolean
+  onConfirm: (newValue: string) => void
+  onOpenChange: (open: boolean) => void
+  onCancel?: () => void
+}
+
+export default function TimeDropdown({
+  value,
+  open,
+  onConfirm,
+  onCancel,
+  onOpenChange
+}: TimeDropdownProps) {
+
+  const [initH, initM] = value.split(':').map((s) => parseInt(s, 10))
+  const [hour, setHour] = useState(isNaN(initH) ? 0 : initH)
+  const [minute, setMinute] = useState(isNaN(initM) ? 0 : initM)
+
+  const handleConfirm = () => {
+    const hh = String(hour).padStart(2, '0')
+    const mm = String(minute).padStart(2, '0')
+    onConfirm(`${hh}:${mm}`)
+    onOpenChange(false)
+  }
+
+  const handleClose = () => {
+    onOpenChange(false)
+    onCancel?.()
+
+    //취소했을 때 반영되지 않게
+    const [resetH, resetM] = value.split(':').map((s) => parseInt(s, 10))
+    setHour(!isNaN(resetH) ? resetH : 0)
+    setMinute(!isNaN(resetM) ? resetM : 0)
+  }
+
+  const timeRef = useRef<HTMLDivElement>(null)
path: src/shared/components/timepicker/TimeDropdown.tsx
user: login: jihostudy
body: 리액트 훅은 맨 위에 다함께 선언해주시면 감사하겠습니다! 
가끔 아래에 만들다보면, 분기처리하다가 Hydration 에러가 발생하기도 합니다.

리액트 훅 : `useRef`, `useState` etc
created_at: 2025-05-07T05:06:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,183 @@
+'use client'
+
+import { useRef, useState } from 'react'
+import styled from '@emotion/styled'
+import { theme } from '@shared/styles/emotion/theme'
+
+import { useOutsideClick } from '@shared/hooks/useOutsideClick/useOutsideClick'
+
+export interface TimeDropdownProps {
+  value: string
+  open: boolean
path: src/shared/components/timepicker/TimeDropdown.tsx
user: login: jihostudy
body: 불리안 값이면 `isOpen` 이 어떤가요?
created_at: 2025-05-08T05:50:49Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,183 @@
+'use client'
+
+import { useRef, useState } from 'react'
+import styled from '@emotion/styled'
+import { theme } from '@shared/styles/emotion/theme'
+
+import { useOutsideClick } from '@shared/hooks/useOutsideClick/useOutsideClick'
+
+export interface TimeDropdownProps {
+  value: string
+  open: boolean
+  onConfirm: (newValue: string) => void
+  onOpenChange: (open: boolean) => void
+  onCancel?: () => void
+}
+
+export default function TimeDropdown({ value, open, onConfirm, onCancel, onOpenChange }: TimeDropdownProps) {
+  const [initH, initM] = value.split(':').map(s => parseInt(s, 10))
+  const [hour, setHour] = useState(isNaN(initH) ? 0 : initH)
+  const [minute, setMinute] = useState(isNaN(initM) ? 0 : initM)
+
+  const handleConfirm = () => {
+    const hh = String(hour).padStart(2, '0')
+    const mm = String(minute).padStart(2, '0')
+    onConfirm(`${hh}:${mm}`)
+    onOpenChange(false)
+  }
+
+  const handleClose = () => {
+    onOpenChange(false)
+    onCancel?.()
+
+    //취소했을 때 반영되지 않게끔 변경
+    const [resetH, resetM] = value.split(':').map(s => parseInt(s, 10))
+    setHour(!isNaN(resetH) ? resetH : 0)
+    setMinute(!isNaN(resetM) ? resetM : 0)
+  }
+
+  const timeRef = useRef<HTMLDivElement>(null)
+
+  useOutsideClick(timeRef as React.RefObject<HTMLElement>, handleClose)
+
+  return (
+    <Wrapper ref={timeRef}>
+      <Trigger
+        onClick={() => {
+          onOpenChange(!open)
+        }}
+      >
+        {String(hour).padStart(2, '0')} : {String(minute).padStart(2, '0')}
+      </Trigger>
+      <Dropdown open={open}>
+        <Columns>
+          <Column>
+            {Array.from({ length: 24 }, (_, i) => i).map(h => (
+              <Option key={h} selected={h === hour} onClick={() => setHour(h)}>
+                {String(h).padStart(2, '0')}
+              </Option>
+            ))}
+          </Column>
+          <Column>
+            {Array.from({ length: 60 }, (_, i) => i).map(m => (
+              <Option key={m} selected={m === minute} onClick={() => setMinute(m)}>
+                {String(m).padStart(2, '0')}
+              </Option>
+            ))}
+          </Column>
+        </Columns>
+        <ActionWrapper>
+          <ActButton
+            onClick={() => {
path: src/shared/components/timepicker/TimeDropdown.tsx
user: login: jihostudy
body: 왜 `handleClose` 참조만 넘기지 않고 화살표 함수로 넘기셨나요?
created_at: 2025-05-08T05:51:31Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,183 @@
+'use client'
+
+import { useRef, useState } from 'react'
+import styled from '@emotion/styled'
+import { theme } from '@shared/styles/emotion/theme'
+
+import { useOutsideClick } from '@shared/hooks/useOutsideClick/useOutsideClick'
+
+export interface TimeDropdownProps {
+  value: string
+  open: boolean
+  onConfirm: (newValue: string) => void
+  onOpenChange: (open: boolean) => void
+  onCancel?: () => void
+}
+
+export default function TimeDropdown({ value, open, onConfirm, onCancel, onOpenChange }: TimeDropdownProps) {
+  const [initH, initM] = value.split(':').map(s => parseInt(s, 10))
+  const [hour, setHour] = useState(isNaN(initH) ? 0 : initH)
+  const [minute, setMinute] = useState(isNaN(initM) ? 0 : initM)
+
+  const handleConfirm = () => {
+    const hh = String(hour).padStart(2, '0')
+    const mm = String(minute).padStart(2, '0')
+    onConfirm(`${hh}:${mm}`)
+    onOpenChange(false)
+  }
+
+  const handleClose = () => {
+    onOpenChange(false)
+    onCancel?.()
+
+    //취소했을 때 반영되지 않게끔 변경
+    const [resetH, resetM] = value.split(':').map(s => parseInt(s, 10))
+    setHour(!isNaN(resetH) ? resetH : 0)
+    setMinute(!isNaN(resetM) ? resetM : 0)
+  }
+
+  const timeRef = useRef<HTMLDivElement>(null)
+
+  useOutsideClick(timeRef as React.RefObject<HTMLElement>, handleClose)
+
+  return (
+    <Wrapper ref={timeRef}>
+      <Trigger
+        onClick={() => {
+          onOpenChange(!open)
+        }}
+      >
+        {String(hour).padStart(2, '0')} : {String(minute).padStart(2, '0')}
+      </Trigger>
+      <Dropdown open={open}>
+        <Columns>
+          <Column>
+            {Array.from({ length: 24 }, (_, i) => i).map(h => (
+              <Option key={h} selected={h === hour} onClick={() => setHour(h)}>
+                {String(h).padStart(2, '0')}
+              </Option>
+            ))}
+          </Column>
+          <Column>
+            {Array.from({ length: 60 }, (_, i) => i).map(m => (
+              <Option key={m} selected={m === minute} onClick={() => setMinute(m)}>
+                {String(m).padStart(2, '0')}
+              </Option>
+            ))}
+          </Column>
+        </Columns>
+        <ActionWrapper>
+          <ActButton
+            onClick={() => {
+              handleClose()
+            }}
+          >
+            Cancel
+          </ActButton>
+          <ActButton
+            primary
+            onClick={() => {
+              handleConfirm()
path: src/shared/components/timepicker/TimeDropdown.tsx
user: login: jihostudy
body: 왜 `handleConfirm` 참조만 넘기지 않고 화살표 함수로 넘기셨나요?
created_at: 2025-05-08T05:51:38Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,177 @@
+'use client'
+
+import { useRef, useState } from 'react'
+import styled from '@emotion/styled'
+import { theme } from '@shared/styles/emotion/theme'
+
+import { useOutsideClick } from '@shared/hooks/useOutsideClick/useOutsideClick'
+
+export interface TimeDropdownProps {
+  value: string
+  open: boolean
+  onConfirm: (newValue: string) => void
+  onOpenChange: (open: boolean) => void
+  onCancel?: () => void
+}
+
+export default function TimeDropdown({
+  value,
+  open,
+  onConfirm,
+  onCancel,
+  onOpenChange
+}: TimeDropdownProps) {
+
+  const [initH, initM] = value.split(':').map((s) => parseInt(s, 10))
+  const [hour, setHour] = useState(isNaN(initH) ? 0 : initH)
+  const [minute, setMinute] = useState(isNaN(initM) ? 0 : initM)
+
+  const handleConfirm = () => {
+    const hh = String(hour).padStart(2, '0')
+    const mm = String(minute).padStart(2, '0')
+    onConfirm(`${hh}:${mm}`)
+    onOpenChange(false)
+  }
+
+  const handleClose = () => {
+    onOpenChange(false)
+    onCancel?.()
+
+    //취소했을 때 반영되지 않게
+    const [resetH, resetM] = value.split(':').map((s) => parseInt(s, 10))
+    setHour(!isNaN(resetH) ? resetH : 0)
+    setMinute(!isNaN(resetM) ? resetM : 0)
+  }
+
+  const timeRef = useRef<HTMLDivElement>(null)
path: src/shared/components/timepicker/TimeDropdown.tsx
user: login: sunsetdust
body: 확인했습니다. 또, 보여주신 영상은 window 와 Mac 의 차이에서 발생하는 것으로 보입니다. 고려해서 스타일 바꾸겠습니다.
created_at: 2025-05-08T08:32:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+'use client'
+
+import { useRef, useState } from 'react'
+import styled from '@emotion/styled'
+import { theme } from '@shared/styles/emotion/theme'
+
+import { useOutsideClick } from '@shared/hooks/useOutsideClick/useOutsideClick'
+
+export interface TimeDropdownProps {
+  value: string
+  open: boolean
path: src/shared/components/timepicker/TimeDropdown.tsx
user: login: sunsetdust
body: 반영하겠습니다.
created_at: 2025-05-08T08:32:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,183 @@
+'use client'
+
+import { useRef, useState } from 'react'
+import styled from '@emotion/styled'
+import { theme } from '@shared/styles/emotion/theme'
+
+import { useOutsideClick } from '@shared/hooks/useOutsideClick/useOutsideClick'
+
+export interface TimeDropdownProps {
+  value: string
+  open: boolean
+  onConfirm: (newValue: string) => void
+  onOpenChange: (open: boolean) => void
+  onCancel?: () => void
+}
+
+export default function TimeDropdown({ value, open, onConfirm, onCancel, onOpenChange }: TimeDropdownProps) {
+  const [initH, initM] = value.split(':').map(s => parseInt(s, 10))
+  const [hour, setHour] = useState(isNaN(initH) ? 0 : initH)
+  const [minute, setMinute] = useState(isNaN(initM) ? 0 : initM)
+
+  const handleConfirm = () => {
+    const hh = String(hour).padStart(2, '0')
+    const mm = String(minute).padStart(2, '0')
+    onConfirm(`${hh}:${mm}`)
+    onOpenChange(false)
+  }
+
+  const handleClose = () => {
+    onOpenChange(false)
+    onCancel?.()
+
+    //취소했을 때 반영되지 않게끔 변경
+    const [resetH, resetM] = value.split(':').map(s => parseInt(s, 10))
+    setHour(!isNaN(resetH) ? resetH : 0)
+    setMinute(!isNaN(resetM) ? resetM : 0)
+  }
+
+  const timeRef = useRef<HTMLDivElement>(null)
+
+  useOutsideClick(timeRef as React.RefObject<HTMLElement>, handleClose)
+
+  return (
+    <Wrapper ref={timeRef}>
+      <Trigger
+        onClick={() => {
+          onOpenChange(!open)
+        }}
+      >
+        {String(hour).padStart(2, '0')} : {String(minute).padStart(2, '0')}
+      </Trigger>
+      <Dropdown open={open}>
+        <Columns>
+          <Column>
+            {Array.from({ length: 24 }, (_, i) => i).map(h => (
+              <Option key={h} selected={h === hour} onClick={() => setHour(h)}>
+                {String(h).padStart(2, '0')}
+              </Option>
+            ))}
+          </Column>
+          <Column>
+            {Array.from({ length: 60 }, (_, i) => i).map(m => (
+              <Option key={m} selected={m === minute} onClick={() => setMinute(m)}>
+                {String(m).padStart(2, '0')}
+              </Option>
+            ))}
+          </Column>
+        </Columns>
+        <ActionWrapper>
+          <ActButton
+            onClick={() => {
path: src/shared/components/timepicker/TimeDropdown.tsx
user: login: sunsetdust
body: 처음 구현했을 때, false를 매개로 실행되는 함수였다가, 이후 구조를 바꾸면서 화살표 함수 수정을 안한 것 같습니다. 현재 수정했습니다. 아래도 같습니다.
created_at: 2025-05-08T11:57:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,196 @@
+'use client'
+
+import { useRef, useState, useEffect } from 'react'
+import styled from '@emotion/styled'
+import { theme } from '@shared/styles/emotion/theme'
+
+export interface TimeDropdownProps {
+  value: string
+  isOpen: boolean
+  onConfirm: (newValue: string) => void
+  onOpenChange: (isOpen: boolean) => void
+  onCancel?: () => void
+}
+
+export default function TimeDropdown({ value, isOpen, onConfirm, onCancel, onOpenChange }: TimeDropdownProps) {
+  const [initH, initM] = value.split(':').map(s => parseInt(s, 10))
+  const [hour, setHour] = useState(isNaN(initH) ? 0 : initH)
+  const [minute, setMinute] = useState(isNaN(initM) ? 0 : initM)
+
+  const wrapperRef = useRef<HTMLDivElement>(null)
+  const dropdownRef = useRef<HTMLDivElement>(null)
+
+  const handleConfirm = () => {
+    const hh = String(hour).padStart(2, '0')
+    const mm = String(minute).padStart(2, '0')
+    onConfirm(`${hh}:${mm}`)
+    onOpenChange(false)
+  }
+
+  const handleClose = () => {
+    onOpenChange(false)
+    onCancel?.()
+
+    //취소했을 때 반영되지 않게끔 변경
+    const [resetH, resetM] = value.split(':').map(s => parseInt(s, 10))
+    setHour(!isNaN(resetH) ? resetH : 0)
+    setMinute(!isNaN(resetM) ? resetM : 0)
+  }
+
+  // 외부 클릭 감지 핸들러
+  useEffect(() => {
+    if (!isOpen) return
+
+    const handleClickOutside = (event: MouseEvent) => {
+      // 드롭다운이 열려있을 때만 처리
+      if (!wrapperRef.current || !dropdownRef.current) return
+      const isClickInWrapper = wrapperRef.current.contains(event.target as Node)
+      const isClickInDropdown = dropdownRef.current.contains(event.target as Node)
+
+      // 클릭이 wrapper의 트리거 버튼에서 발생했는지 확인
+      const triggerElement = wrapperRef.current.querySelector('button')
+      const isClickOnTrigger = triggerElement && triggerElement.contains(event.target as Node)
+
+      // 외부 클릭인 경우에만 닫기
+      if (!isClickInWrapper || (isClickInWrapper && !isClickInDropdown && !isClickOnTrigger)) {
+        handleClose()
+      }
+    }
+    document.addEventListener('mousedown', handleClickOutside)
+
+    return () => {
+      //이벤트리스너 제거
+      document.removeEventListener('mousedown', handleClickOutside)
+    }
+  }, [isOpen])
+
+  return (
+    <Wrapper ref={wrapperRef}>
+      <Trigger
+        onClick={() => {
+          onOpenChange(!isOpen)
+        }}
+      >
+        {String(hour).padStart(2, '0')} : {String(minute).padStart(2, '0')}
+      </Trigger>
+      <Dropdown isOpen={isOpen} ref={dropdownRef}>
+        <Columns>
+          <Column>
+            {Array.from({ length: 24 }, (_, i) => i).map(h => (
+              <Option key={h} selected={h === hour} onClick={() => setHour(h)}>
+                {String(h).padStart(2, '0')}
+              </Option>
+            ))}
+          </Column>
+          <Column style={{ marginLeft: '2px' }}>
+            {Array.from({ length: 60 }, (_, i) => i).map(m => (
+              <Option key={m} selected={m === minute} onClick={() => setMinute(m)}>
+                {String(m).padStart(2, '0')}
+              </Option>
+            ))}
+          </Column>
+        </Columns>
+        <ActionWrapper>
+          <ActButton onClick={handleClose}>Cancel</ActButton>
+          <ActButton primary onClick={handleConfirm}>
+            OK
path: src/shared/components/timepicker/TimeDropdown.tsx
user: login: jihostudy
body: 확인/취소로 변경하면 좋을 것 같습니다! 
created_at: 2025-05-09T06:34:47Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,196 @@
+'use client'
+
+import { useRef, useState, useEffect } from 'react'
+import styled from '@emotion/styled'
+import { theme } from '@shared/styles/emotion/theme'
+
+export interface TimeDropdownProps {
+  value: string
+  isOpen: boolean
+  onConfirm: (newValue: string) => void
+  onOpenChange: (isOpen: boolean) => void
+  onCancel?: () => void
+}
+
+export default function TimeDropdown({ value, isOpen, onConfirm, onCancel, onOpenChange }: TimeDropdownProps) {
+  const [initH, initM] = value.split(':').map(s => parseInt(s, 10))
+  const [hour, setHour] = useState(isNaN(initH) ? 0 : initH)
+  const [minute, setMinute] = useState(isNaN(initM) ? 0 : initM)
+
+  const wrapperRef = useRef<HTMLDivElement>(null)
+  const dropdownRef = useRef<HTMLDivElement>(null)
+
+  const handleConfirm = () => {
+    const hh = String(hour).padStart(2, '0')
+    const mm = String(minute).padStart(2, '0')
+    onConfirm(`${hh}:${mm}`)
+    onOpenChange(false)
+  }
+
+  const handleClose = () => {
+    onOpenChange(false)
+    onCancel?.()
+
+    //취소했을 때 반영되지 않게끔 변경
+    const [resetH, resetM] = value.split(':').map(s => parseInt(s, 10))
+    setHour(!isNaN(resetH) ? resetH : 0)
+    setMinute(!isNaN(resetM) ? resetM : 0)
+  }
+
+  // 외부 클릭 감지 핸들러
+  useEffect(() => {
+    if (!isOpen) return
+
+    const handleClickOutside = (event: MouseEvent) => {
+      // 드롭다운이 열려있을 때만 처리
+      if (!wrapperRef.current || !dropdownRef.current) return
+      const isClickInWrapper = wrapperRef.current.contains(event.target as Node)
+      const isClickInDropdown = dropdownRef.current.contains(event.target as Node)
+
+      // 클릭이 wrapper의 트리거 버튼에서 발생했는지 확인
+      const triggerElement = wrapperRef.current.querySelector('button')
+      const isClickOnTrigger = triggerElement && triggerElement.contains(event.target as Node)
+
+      // 외부 클릭인 경우에만 닫기
+      if (!isClickInWrapper || (isClickInWrapper && !isClickInDropdown && !isClickOnTrigger)) {
+        handleClose()
+      }
+    }
+    document.addEventListener('mousedown', handleClickOutside)
+
+    return () => {
+      //이벤트리스너 제거
+      document.removeEventListener('mousedown', handleClickOutside)
+    }
+  }, [isOpen])
+
+  return (
+    <Wrapper ref={wrapperRef}>
+      <Trigger
+        onClick={() => {
+          onOpenChange(!isOpen)
+        }}
+      >
+        {String(hour).padStart(2, '0')} : {String(minute).padStart(2, '0')}
+      </Trigger>
+      <Dropdown isOpen={isOpen} ref={dropdownRef}>
+        <Columns>
+          <Column>
+            {Array.from({ length: 24 }, (_, i) => i).map(h => (
+              <Option key={h} selected={h === hour} onClick={() => setHour(h)}>
+                {String(h).padStart(2, '0')}
+              </Option>
+            ))}
+          </Column>
+          <Column style={{ marginLeft: '2px' }}>
+            {Array.from({ length: 60 }, (_, i) => i).map(m => (
+              <Option key={m} selected={m === minute} onClick={() => setMinute(m)}>
+                {String(m).padStart(2, '0')}
+              </Option>
+            ))}
+          </Column>
+        </Columns>
+        <ActionWrapper>
+          <ActButton onClick={handleClose}>Cancel</ActButton>
+          <ActButton primary onClick={handleConfirm}>
+            OK
path: src/shared/components/timepicker/TimeDropdown.tsx
user: login: sunsetdust
body: 반영 후 머지 하겠습니다.
created_at: 2025-05-10T04:31:54Z
author_association: CONTRIBUTOR

PR comments:
user: login: jihostudy
created_at: 2025-05-08T05:44:04Z
body: <img width="282" alt="image" src="https://github.com/user-attachments/assets/4bbaef0b-b723-4247-ac97-b3f505760938" />

제 화면에서는 이렇게 width 크기가 작은걸로 보입니다.
author_association: MEMBER
user: login: jihostudy
created_at: 2025-05-08T05:50:09Z
body: 
https://github.com/user-attachments/assets/974d18dd-010e-41c8-aea8-cfd60a157f80

제 컴퓨터에서는 아예 작동이 안됩니다 !!
author_association: MEMBER
user: login: jihostudy
created_at: 2025-05-09T06:37:36Z
body: 마지막으로, 창이 열리는 조건이 `TimeDropdown`을 클릭했을 때가 아닌, 하나의 시간 박스를 클릭했을때로 변경해주실 수 있을까요? 

유저가 대충 찍어도 열리게끔 했으면 좋겠습니다.

_하나의 시간 박스 사진 첨부합니다_
<img width="150" alt="image" src="https://github.com/user-attachments/assets/a3d4a698-932e-4d28-a160-be003f70d8f4" />

author_association: MEMBER
user: login: sunsetdust
created_at: 2025-05-10T04:31:39Z
body: > 마지막으로, 창이 열리는 조건이 `TimeDropdown`을 클릭했을 때가 아닌, 하나의 시간 박스를 클릭했을때로 변경해주실 수 있을까요?
> 
> 유저가 대충 찍어도 열리게끔 했으면 좋겠습니다.
> 
> _하나의 시간 박스 사진 첨부합니다_ <img alt="image" width="150" src="https://private-user-images.githubusercontent.com/110150963/442007337-a3d4a698-932e-4d28-a160-be003f70d8f4.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY4NTE3NTgsIm5iZiI6MTc0Njg1MTQ1OCwicGF0aCI6Ii8xMTAxNTA5NjMvNDQyMDA3MzM3LWEzZDRhNjk4LTkzMmUtNGQyOC1hMTYwLWJlMDAzZjcwZDhmNC5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUwNTEwJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MDUxMFQwNDMwNThaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT03NDY0ZGQwMGY0M2ZmYTJhOWQ0YzE3YmFhY2M4ZTQ2ODY5NjVkNThkMTlmNDk5MGVkODg5YmYzYjk1NGMxMTdhJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.9SYC1zkw7GnbNij16yuAglj3sT3_KXyXiA7RSCMaum8">

반영 후 머지 하겠습니다
author_association: CONTRIBUTOR