Repository: wonsik3686/genie-docs
Pull request #28: Feature/27 문서 검색 기능 구현
Author: login: wonsik3686
Labels: name: enhancement
description: New feature or request
Created: 2025-03-25T10:50:08Z | Merged: 2025-03-25T12:43:08Z

# Feature/27 문서 검색 기능 구현 #27

## 개요 (Summary)

Notion 페이지 검색 기능을 구현하고, 검색 성능을 최적화하기 위해 디바운스 기능을 추가했습니다.

## 변경 사항 (Changes)

### 주요 기능/로직:
1. **Notion 검색 기능 구현**
   - `NotionSearchCommand` 컴포넌트 추가
   - 검색 API 엔드포인트 구현 (`/api/notion/search`)
   - 검색 결과 표시 및 페이지 이동 기능

2. **디바운스 적용**
   - `useDebounce` 커스텀 훅 구현
   - 검색 입력에 300ms 디바운스 적용
   - 불필요한 API 호출 감소

3. **UI/UX 개선**
   - 모바일/데스크톱 대응 검색 UI
   - 로딩/에러 상태 처리
   - 단축키(⌘K) 지원

### 버그 수정:
- 검색 입력 시 과도한 API 호출 문제 해결
- 검색 결과 표시 지연 문제 해결

### 기타:
- `useNotionIntegrationStatus` 훅 추가
- Notion API 타입 정의 추가

## 스크린샷

- debounce 적용 전
![Mar-25-2025 19-44-42](https://github.com/user-attachments/assets/4f846b05-7764-4831-8015-620c52d796d1)

- debounce 적용 후
![Mar-25-2025 19-44-13](https://github.com/user-attachments/assets/d0e87a77-a682-47b1-9d4d-dc01d55043b9)


## 관련 이슈

- Closes #27

## 기타 (Additional context)

### 성능 개선
- API 호출 수 90% 감소
- 서버 부하 감소

### 테스트 결과
| 시나리오 | 디바운스 적용 전 | 디바운스 적용 후 |
|---------|------------------|------------------|
| 10글자 연속 입력 | 10회 API 호출 | 1회 API 호출 |
| 서버 부하 | 높음 | 낮음 |


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit


- **신규 기능**
  - Notion 페이지 검색 기능이 추가되어, 명령 대화 상자를 통해 손쉽게 검색할 수 있습니다.
  - 헤더와 네비게이션 메뉴가 모바일과 데스크톱 환경에 맞게 최적화되어, 보다 직관적인 사용 경험을 제공합니다.
  - 대시보드의 통합 상태 표시가 개선되어, Notion 및 OpenAI 연동 상태를 더욱 명확하게 확인할 수 있습니다.
  - 새로운 커맨드 대화 상자가 도입되어, 사용자에게 명령 입력 기능을 제공합니다.

- **리팩터**
  - 내부 구조가 정리되어, 전반적인 인터페이스 성능과 안정성이 향상되었습니다.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Linked issue #27: [Feature request] 기능 문서 검색 기능 구현
## 배경/목표
- 사용자 입력에 따라 실시간 검색되는 문서 제목 자동완성 기능

## 주요 요구사항
- 문서 검색창
- 문서 검색을 위한 타이핑을 하면 Notion API 를 활용하여 문서 검색 결과 노출

## 완료 기준
- 사용자가 필요한 문서 제목을 타이핑하면 관련 문서를 찾아 결과를 노출


Diff:
diff --git a/package-lock.json b/package-lock.json
index 3d54c75..3c74f48 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -21,12 +21,14 @@
         "@radix-ui/react-separator": "^1.1.2",
         "@radix-ui/react-slot": "^1.1.2",
         "@radix-ui/react-tooltip": "^1.1.8",
+        "@radix-ui/react-visually-hidden": "^1.1.2",
         "@react-three/drei": "^10.0.4",
         "@react-three/fiber": "^9.1.0",
         "@tanstack/react-query": "^5.66.9",
         "@tanstack/react-query-devtools": "^5.66.9",
         "class-variance-authority": "^0.7.1",
         "clsx": "^2.1.1",
+        "cmdk": "^1.1.1",
         "cookie": "^1.0.2",
         "js-cookie": "^3.0.5",
         "lucide-react": "^0.475.0",
@@ -71,7 +73,6 @@
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
       "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=10"
@@ -711,7 +712,6 @@
       "version": "8.0.2",
       "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
       "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
-      "dev": true,
       "license": "ISC",
       "dependencies": {
         "string-width": "^5.1.2",
@@ -729,7 +729,6 @@
       "version": "0.3.8",
       "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
       "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@jridgewell/set-array": "^1.2.1",
@@ -744,7 +743,6 @@
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
       "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=6.0.0"
@@ -754,7 +752,6 @@
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
       "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=6.0.0"
@@ -764,14 +761,12 @@
       "version": "1.5.0",
       "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
       "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/@jridgewell/trace-mapping": {
       "version": "0.3.25",
       "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
       "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@jridgewell/resolve-uri": "^3.1.0",
@@ -944,7 +939,6 @@
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
       "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@nodelib/fs.stat": "2.0.5",
@@ -958,7 +952,6 @@
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
       "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">= 8"
@@ -968,7 +961,6 @@
       "version": "1.2.8",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
       "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@nodelib/fs.scandir": "2.1.5",
@@ -1005,7 +997,6 @@
       "version": "0.11.0",
       "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
       "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "engines": {
@@ -2085,7 +2076,6 @@
       "version": "19.0.10",
       "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz",
       "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "csstype": "^3.0.2"
@@ -2095,7 +2085,7 @@
       "version": "19.0.4",
       "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.4.tgz",
       "integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==",
-      "dev": true,
+      "devOptional": true,
       "license": "MIT",
       "peerDependencies": {
         "@types/react": "^19.0.0"
@@ -2492,7 +2482,6 @@
       "version": "6.1.0",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
       "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=12"
@@ -2505,7 +2494,6 @@
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
       "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "color-convert": "^2.0.1"
@@ -2521,14 +2509,12 @@
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
       "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/anymatch": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
       "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
-      "dev": true,
       "license": "ISC",
       "dependencies": {
         "normalize-path": "^3.0.0",
@@ -2542,7 +2528,6 @@
       "version": "5.0.2",
       "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
       "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/argparse": {
@@ -2804,7 +2789,6 @@
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
       "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/base64-js": {
@@ -2840,7 +2824,6 @@
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
       "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=8"
@@ -2864,7 +2847,6 @@
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
       "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "fill-range": "^7.1.1"
@@ -2971,7 +2953,6 @@
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
       "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">= 6"
@@ -3077,7 +3058,6 @@
       "version": "3.6.0",
       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
       "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "anymatch": "~3.1.2",
@@ -3102,7 +3082,6 @@
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
       "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
-      "dev": true,
       "license": "ISC",
       "dependencies": {
         "is-glob": "^4.0.1"
@@ -3196,6 +3175,22 @@
         "node": ">=6"
       }
     },
+    "node_modules/cmdk": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz",
+      "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-compose-refs": "^1.1.1",
+        "@radix-ui/react-dialog": "^1.1.6",
+        "@radix-ui/react-id": "^1.1.0",
+        "@radix-ui/react-primitive": "^2.0.2"
+      },
+      "peerDependencies": {
+        "react": "^18 || ^19 || ^19.0.0-rc",
+        "react-dom": "^18 || ^19 || ^19.0.0-rc"
+      }
+    },
     "node_modules/color": {
       "version": "4.2.3",
       "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
@@ -3214,7 +3209,6 @@
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
       "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-      "devOptional": true,
       "license": "MIT",
       "dependencies": {
         "color-name": "~1.1.4"
@@ -3227,7 +3221,6 @@
       "version": "1.1.4",
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
       "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-      "devOptional": true,
       "license": "MIT"
     },
     "node_modules/color-string": {
@@ -3274,7 +3267,6 @@
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
       "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">= 6"
@@ -3332,7 +3324,6 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
       "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
-      "dev": true,
       "license": "MIT",
       "bin": {
         "cssesc": "bin/cssesc"
@@ -3345,7 +3336,6 @@
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
       "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/damerau-levenshtein": {
@@ -3542,14 +3532,12 @@
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
       "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
-      "dev": true,
       "license": "Apache-2.0"
     },
     "node_modules/dlv": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
       "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/doctrine": {
@@ -3589,14 +3577,12 @@
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
       "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/emoji-regex": {
       "version": "9.2.2",
       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
       "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/enhanced-resolve": {
@@ -4396,7 +4382,6 @@
       "version": "1.19.0",
       "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz",
       "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==",
-      "dev": true,
       "license": "ISC",
       "dependencies": {
         "reusify": "^1.0.4"
@@ -4425,7 +4410,6 @@
       "version": "7.1.1",
       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
       "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "to-regex-range": "^5.0.1"
@@ -4492,7 +4476,6 @@
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
       "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==",
-      "dev": true,
       "license": "ISC",
       "dependencies": {
         "cross-spawn": "^7.0.0",
@@ -4570,7 +4553,6 @@
       "version": "2.3.3",
       "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
       "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
-      "dev": true,
       "hasInstallScript": true,
       "license": "MIT",
       "optional": true,
@@ -4728,7 +4710,6 @@
       "version": "10.4.5",
       "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
       "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
-      "dev": true,
       "license": "ISC",
       "dependencies": {
         "foreground-child": "^3.1.0",
@@ -4749,7 +4730,6 @@
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
       "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
-      "dev": true,
       "license": "ISC",
       "dependencies": {
         "is-glob": "^4.0.3"
@@ -4762,7 +4742,6 @@
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
       "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "balanced-match": "^1.0.0"
@@ -4772,7 +4751,6 @@
       "version": "9.0.5",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
       "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
-      "dev": true,
       "license": "ISC",
       "dependencies": {
         "brace-expansion": "^2.0.1"
@@ -5201,7 +5179,6 @@
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
       "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "binary-extensions": "^2.0.0"
@@ -5254,7 +5231,6 @@
       "version": "2.16.1",
       "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
       "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "hasown": "^2.0.2"
@@ -5315,7 +5291,6 @@
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
       "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
@@ -5341,7 +5316,6 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
       "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=8"
@@ -5370,7 +5344,6 @@
       "version": "4.0.3",
       "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
       "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "is-extglob": "^2.1.1"
@@ -5406,7 +5379,6 @@
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
       "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=0.12.0"
@@ -5652,7 +5624,6 @@
       "version": "3.4.3",
       "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
       "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
-      "dev": true,
       "license": "BlueOak-1.0.0",
       "dependencies": {
         "@isaacs/cliui": "^8.0.2"
@@ -5668,7 +5639,6 @@
       "version": "1.21.7",
       "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
       "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
-      "dev": true,
       "license": "MIT",
       "bin": {
         "jiti": "bin/jiti.js"
@@ -5810,7 +5780,6 @@
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
       "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=14"
@@ -5823,7 +5792,6 @@
       "version": "1.2.4",
       "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
       "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/lint-staged": {
@@ -6110,7 +6078,6 @@
       "version": "10.4.3",
       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
       "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
-      "dev": true,
       "license": "ISC"
     },
     "node_modules/lucide-react": {
@@ -6305,7 +6272,6 @@
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
       "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">= 8"
@@ -6772,7 +6738,6 @@
       "version": "4.0.8",
       "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
       "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "braces": "^3.0.3",
@@ -6856,7 +6821,6 @@
       "version": "7.1.2",
       "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
       "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
-      "dev": true,
       "license": "ISC",
       "engines": {
         "node": ">=16 || 14 >=14.17"
@@ -6913,7 +6877,6 @@
       "version": "2.7.0",
       "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
       "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "any-promise": "^1.0.0",
@@ -7081,7 +7044,6 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
       "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
@@ -7120,7 +7082,6 @@
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
       "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
@@ -7130,7 +7091,6 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
       "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">= 6"
@@ -7381,7 +7341,6 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
       "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
-      "dev": true,
       "license": "BlueOak-1.0.0"
     },
     "node_modules/parent-module": {
@@ -7445,14 +7404,12 @@
       "version": "1.0.7",
       "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
       "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/path-scurry": {
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
       "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
-      "dev": true,
       "license": "BlueOak-1.0.0",
       "dependencies": {
         "lru-cache": "^10.2.0",
@@ -7475,7 +7432,6 @@
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
       "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=8.6"
@@ -7501,7 +7457,6 @@
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
       "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
@@ -7511,7 +7466,6 @@
       "version": "4.0.6",
       "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
       "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">= 6"
@@ -7531,7 +7485,6 @@
       "version": "8.5.3",
       "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
       "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
-      "dev": true,
       "funding": [
         {
           "type": "opencollective",
@@ -7560,7 +7513,6 @@
       "version": "15.1.0",
       "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
       "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "postcss-value-parser": "^4.0.0",
@@ -7578,7 +7530,6 @@
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
       "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "camelcase-css": "^2.0.1"
@@ -7598,7 +7549,6 @@
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
       "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
-      "dev": true,
       "funding": [
         {
           "type": "opencollective",
@@ -7634,7 +7584,6 @@
       "version": "6.2.0",
       "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
       "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
-      "dev": true,
       "funding": [
         {
           "type": "opencollective",
@@ -7660,7 +7609,6 @@
       "version": "6.1.2",
       "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
       "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "cssesc": "^3.0.0",
@@ -7674,7 +7622,6 @@
       "version": "4.2.0",
       "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
       "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/potpack": {
@@ -7847,7 +7794,6 @@
       "version": "1.2.3",
       "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
       "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
-      "dev": true,
       "funding": [
         {
           "type": "github",
@@ -8038,7 +7984,6 @@
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
       "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "pify": "^2.3.0"
@@ -8048,7 +7993,6 @@
       "version": "3.6.0",
       "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
       "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "picomatch": "^2.2.1"
@@ -8153,7 +8097,6 @@
       "version": "1.22.10",
       "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
       "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "is-core-module": "^2.16.0",
@@ -8227,7 +8170,6 @@
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
       "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "iojs": ">=1.0.0",
@@ -8245,7 +8187,6 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
       "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
-      "dev": true,
       "funding": [
         {
           "type": "github",
@@ -8529,7 +8470,6 @@
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
       "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
-      "dev": true,
       "license": "ISC",
       "engines": {
         "node": ">=14"
@@ -8675,7 +8615,6 @@
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
       "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "eastasianwidth": "^0.2.0",
@@ -8694,7 +8633,6 @@
       "version": "4.2.3",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
       "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "emoji-regex": "^8.0.0",
@@ -8709,7 +8647,6 @@
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
       "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=8"
@@ -8719,14 +8656,12 @@
       "version": "8.0.0",
       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
       "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/string-width-cjs/node_modules/strip-ansi": {
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
       "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "ansi-regex": "^5.0.1"
@@ -8866,7 +8801,6 @@
       "version": "7.1.0",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
       "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "ansi-regex": "^6.0.1"
@@ -8883,7 +8817,6 @@
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
       "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "ansi-regex": "^5.0.1"
@@ -8896,7 +8829,6 @@
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
       "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=8"
@@ -8983,7 +8915,6 @@
       "version": "3.35.0",
       "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
       "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@jridgewell/gen-mapping": "^0.3.2",
@@ -9019,7 +8950,6 @@
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
       "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">= 0.4"
@@ -9068,7 +8998,6 @@
       "version": "3.4.17",
       "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
       "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@alloc/quick-lru": "^5.2.0",
@@ -9115,7 +9044,6 @@
       "version": "3.3.3",
       "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
       "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@nodelib/fs.stat": "^2.0.2",
@@ -9132,7 +9060,6 @@
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
       "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
-      "dev": true,
       "license": "ISC",
       "dependencies": {
         "is-glob": "^4.0.1"
@@ -9155,7 +9082,6 @@
       "version": "3.3.1",
       "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
       "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "any-promise": "^1.0.0"
@@ -9165,7 +9091,6 @@
       "version": "1.6.0",
       "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
       "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "thenify": ">= 3.1.0 < 4"
@@ -9261,7 +9186,6 @@
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
       "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "is-number": "^7.0.0"
@@ -9343,7 +9267,6 @@
       "version": "0.1.13",
       "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
       "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
-      "dev": true,
       "license": "Apache-2.0"
     },
     "node_modules/tsconfig-paths": {
@@ -9685,7 +9608,6 @@
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
       "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/utility-types": {
@@ -9878,7 +9800,6 @@
       "version": "8.1.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
       "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "ansi-styles": "^6.1.0",
@@ -9897,7 +9818,6 @@
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
       "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "ansi-styles": "^4.0.0",
@@ -9915,7 +9835,6 @@
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
       "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=8"
@@ -9925,14 +9844,12 @@
       "version": "8.0.0",
       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
       "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/wrap-ansi-cjs/node_modules/string-width": {
       "version": "4.2.3",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
       "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "emoji-regex": "^8.0.0",
@@ -9947,7 +9864,6 @@
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
       "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "ansi-regex": "^5.0.1"
@@ -9960,7 +9876,6 @@
       "version": "6.2.1",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
       "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=12"
@@ -9973,7 +9888,6 @@
       "version": "2.7.0",
       "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
       "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==",
-      "dev": true,
       "license": "ISC",
       "bin": {
         "yaml": "bin.mjs"
diff --git a/package.json b/package.json
index a57f9e6..be6ee9f 100644
--- a/package.json
+++ b/package.json
@@ -30,12 +30,14 @@
     "@radix-ui/react-separator": "^1.1.2",
     "@radix-ui/react-slot": "^1.1.2",
     "@radix-ui/react-tooltip": "^1.1.8",
+    "@radix-ui/react-visually-hidden": "^1.1.2",
     "@react-three/drei": "^10.0.4",
     "@react-three/fiber": "^9.1.0",
     "@tanstack/react-query": "^5.66.9",
     "@tanstack/react-query-devtools": "^5.66.9",
     "class-variance-authority": "^0.7.1",
     "clsx": "^2.1.1",
+    "cmdk": "^1.1.1",
     "cookie": "^1.0.2",
     "js-cookie": "^3.0.5",
     "lucide-react": "^0.475.0",
diff --git a/src/app/api/notion/search/route.ts b/src/app/api/notion/search/route.ts
new file mode 100644
index 0000000..ceaf61d
--- /dev/null
+++ b/src/app/api/notion/search/route.ts
@@ -0,0 +1,39 @@
+import { Client } from '@notionhq/client';
+import { parse } from 'cookie';
+import { NextRequest, NextResponse } from 'next/server';
+
+type SearchRequestBody = {
+  query: string;
+  filterType: 'page' | 'database';
+  pageSize?: number;
+};
+
+export async function POST(req: NextRequest) {
+  try {
+    const { query, filterType, pageSize } =
+      (await req.json()) as SearchRequestBody;
+    const cookies = parse(req.headers.get('cookie') || '');
+    const notionApiKey = cookies.notionApiKey;
+    const notion = new Client({ auth: notionApiKey });
+
+    const response = await notion.search({
+      query,
+      filter: {
+        value: filterType,
+        property: 'object',
+      },
+      sort: {
+        direction: 'ascending',
+        timestamp: 'last_edited_time',
+      },
+      page_size: pageSize || 100,
+    });
+
+    return NextResponse.json(response);
+  } catch (error) {
+    return NextResponse.json(
+      { error: '검색 중 오류가 발생했습니다.', errorMessage: error },
+      { status: 500 }
+    );
+  }
+}
diff --git a/src/components/dashboard/IntegrationStatus.tsx b/src/components/dashboard/IntegrationStatus.tsx
index 60567de..514c2ae 100644
--- a/src/components/dashboard/IntegrationStatus.tsx
+++ b/src/components/dashboard/IntegrationStatus.tsx
@@ -1,21 +1,13 @@
 'use client';
 
 import { Card, CardContent, CardHeader } from '@/components/ui/card';
+import useNotionIntegrationStatus from '@/hooks/notion/useNotionIntegrationStatus';
 import { cn } from '@/lib/utils';
-import { useSettingStore } from '@/store/settingStore';
-import Cookies from 'js-cookie';
-import React, { useEffect, useState } from 'react';
+import React from 'react';
 
 const IntegrationStatus: React.FC = () => {
-  const [notionApiKey, setNotionApiKey] = useState<string | undefined>();
-  const [openAIApiKey, setOpenAIApiKey] = useState<string | undefined>();
-
-  useEffect(() => {
-    setNotionApiKey(Cookies.get('notionApiKey'));
-    setOpenAIApiKey(Cookies.get('openAiApiKey'));
-  }, []);
-
-  const { notionPageId } = useSettingStore();
+  const { isNotionIntegrated, isOpenAIIntegrated } =
+    useNotionIntegrationStatus();
 
   return (
     <Card className="w-full md:w-1/2">
@@ -26,19 +18,19 @@ const IntegrationStatus: React.FC = () => {
             <div
               className={cn(
                 'h-2 w-2 rounded-full',
-                notionApiKey && notionPageId ? 'bg-green-500' : 'bg-red-500'
+                isNotionIntegrated ? 'bg-green-500' : 'bg-red-500'
               )}
             />
-            <p>노션 연동 {notionApiKey && notionPageId ? '완료' : '미완료'}</p>
+            <p>노션 연동 {isNotionIntegrated ? '완료' : '미완료'}</p>
           </div>
           <div className="flex items-center gap-2">
             <div
               className={cn(
                 'h-2 w-2 rounded-full',
-                openAIApiKey ? 'bg-green-500' : 'bg-red-500'
+                isOpenAIIntegrated ? 'bg-green-500' : 'bg-red-500'
               )}
             />
-            <p>OpenAI 연동 {openAIApiKey ? '완료' : '미완료'}</p>
+            <p>OpenAI 연동 {isOpenAIIntegrated ? '완료' : '미완료'}</p>
           </div>
         </div>
       </CardContent>
diff --git a/src/components/layouts/Header.tsx b/src/components/layouts/Header.tsx
index 000e5cb..5113428 100644
--- a/src/components/layouts/Header.tsx
+++ b/src/components/layouts/Header.tsx
@@ -1,17 +1,31 @@
 'use client';
 
-import { Moon, Sun } from 'lucide-react';
+import { Menu, Moon, Sun } from 'lucide-react';
 import { useTheme } from 'next-themes';
 import { useEffect } from 'react';
 
 import { Button } from '@/components/ui/button';
 import { SidebarTrigger } from '@/components/ui/sidebar';
+import useNotionIntegrationStatus from '@/hooks/notion/useNotionIntegrationStatus';
+import { useIsMobile } from '@/hooks/shadcn/use-mobile';
 import Link from 'next/link';
 import { usePathname } from 'next/navigation';
+import NotionSearchCommand from '../notion/NotionSearchCommand';
+import {
+  NavigationMenu,
+  NavigationMenuContent,
+  NavigationMenuItem,
+  NavigationMenuLink,
+  NavigationMenuList,
+  NavigationMenuTrigger,
+} from '../ui/navigation-menu';
 
 function Header() {
   const { theme, setTheme } = useTheme();
   const pathname = usePathname();
+  const isMobile = useIsMobile();
+  const { isNotionIntegrated } = useNotionIntegrationStatus();
+
   const menuItems = [
     {
       label: '대시보드',
@@ -42,20 +56,48 @@ function Header() {
             </span>
           </Link>
         </Button>
-        <nav className="space-x-4 xs:space-x-0">
-          {menuItems.map((item) => (
-            <Button
-              className={`${pathname === item.href ? 'font-bold' : 'font-semibold text-gray-600 dark:text-gray-300'}`}
-              variant="ghost"
-              asChild
-              key={item.href}
-            >
-              <Link href={item.href}>{item.label}</Link>
-            </Button>
-          ))}
-        </nav>
+        {!isMobile && (
+          <nav className="space-x-4 xs:space-x-0">
+            {menuItems.map((item) => (
+              <Button
+                className={`${pathname === item.href ? 'font-bold' : 'font-semibold text-gray-600 dark:text-gray-300'}`}
+                variant="ghost"
+                asChild
+                key={item.href}
+              >
+                <Link href={item.href}>{item.label}</Link>
+              </Button>
+            ))}
+          </nav>
+        )}
       </div>
-      <div className="flex items-center space-x-4">
+      <div className="absolute right-4 flex items-center space-x-4">
+        {isMobile && (
+          <NavigationMenu>
+            <NavigationMenuList>
+              <NavigationMenuItem>
+                <NavigationMenuTrigger>
+                  <Menu className="h-[1.2rem] w-[1.2rem]" />
+                </NavigationMenuTrigger>
+                <NavigationMenuContent>
+                  {menuItems.map((item) => (
+                    <Link key={item.href} href={item.href}>
+                      <NavigationMenuLink asChild>
+                        <Button
+                          variant="ghost"
+                          className="w-full px-10 text-lg-bold"
+                        >
+                          {item.label}
+                        </Button>
+                      </NavigationMenuLink>
+                    </Link>
+                  ))}
+                </NavigationMenuContent>
+              </NavigationMenuItem>
+            </NavigationMenuList>
+          </NavigationMenu>
+        )}
+        {isNotionIntegrated && <NotionSearchCommand />}
         <Button
           variant="outline"
           size="icon"
diff --git a/src/components/notion/NotionSearchCommand.tsx b/src/components/notion/NotionSearchCommand.tsx
new file mode 100644
index 0000000..e4f350d
--- /dev/null
+++ b/src/components/notion/NotionSearchCommand.tsx
@@ -0,0 +1,129 @@
+'use client';
+
+import { Loader2, Search } from 'lucide-react';
+
+import {
+  CommandDialog,
+  CommandInput,
+  CommandList,
+} from '@/components/ui/command';
+import { useIsMobile } from '@/hooks/shadcn/use-mobile';
+import useDebounce from '@/hooks/utils/useDebounce';
+import { useNotionSearch } from '@/queries/notion.queries';
+import Link from 'next/link';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { Button } from '../ui/button';
+
+export default function NotionSearchCommand() {
+  const [open, setOpen] = useState(false);
+  const [searchQuery, setSearchQuery] = useState('');
+  const isMobile = useIsMobile();
+  const router = useRouter();
+  const { mutate: searchNotion, data, isError, isPending } = useNotionSearch();
+
+  const debouncedSearchQuery = useDebounce(searchQuery, 300);
+
+  useEffect(() => {
+    const down = (e: KeyboardEvent) => {
+      if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
+        e.preventDefault();
+        setOpen((open) => !open);
+      }
+    };
+
+    document.addEventListener('keydown', down);
+    return () => document.removeEventListener('keydown', down);
+  }, []);
+
+  useEffect(() => {
+    if (debouncedSearchQuery.trim()) {
+      handleSearch(debouncedSearchQuery);
+    }
+  }, [debouncedSearchQuery]);
+
+  const handleSearch = (value: string) => {
+    searchNotion({ query: value, filterType: 'page', pageSize: 100 });
+  };
+
+  return (
+    <>
+      {isMobile ? (
+        <Button variant="outline" size="icon" onClick={() => setOpen(true)}>
+          <Search className="h-4 w-4" />
+        </Button>
+      ) : (
+        <Button
+          className="w-full justify-start rounded-lg px-4 py-1 text-left font-normal text-muted-foreground hover:text-foreground"
+          variant="outline"
+          size="icon"
+          onClick={() => setOpen(true)}
+        >
+          <p className="flex w-48 items-center justify-between text-sm">
+            <span className="flex items-center gap-1 text-xs">
+              <Search className="h-4 w-4" />
+              Notion 페이지 검색..
+            </span>
+            <kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
+              <span className="text-xs">⌘</span>K
+            </kbd>
+          </p>
+        </Button>
+      )}
+      <CommandDialog
+        open={open}
+        onOpenChange={setOpen}
+        commandValue={searchQuery}
+      >
+        <CommandInput
+          placeholder="페이지 제목 검색..."
+          onValueChange={(value) => {
+            setSearchQuery(value);
+          }}
+        />
+
+        <CommandList>
+          {data?.results.map((result) => {
+            return (
+              <Button
+                key={result.id}
+                variant="ghost"
+                className="w-full justify-start"
+                onClick={() => {
+                  router.push(`/dashboard/notion/page?pageId=${result.id}`);
+                }}
+                asChild
+              >
+                <Link href={`/dashboard/notion/page?pageId=${result.id}`}>
+                  <p className="text-left">
+                    {result.properties?.title?.title?.[0]?.plain_text ||
+                      '제목 없음'}
+                  </p>
+                </Link>
+              </Button>
+            );
+          })}
+          {isPending && (
+            <div className="flex w-full items-center justify-center py-4">
+              <Loader2 className="h-4 w-4 animate-spin" />
+            </div>
+          )}
+          {isError && (
+            <div className="flex w-full items-center justify-center py-4">
+              <p className="text-center text-sm text-muted-foreground">
+                검색 중 오류가 발생했습니다.
+              </p>
+            </div>
+          )}
+          {data?.results.length === 0 && (
+            <div className="flex w-full items-center justify-center py-4">
+              <p className="text-center text-sm text-muted-foreground">
+                검색 결과가 없습니다.
+              </p>
+            </div>
+          )}
+        </CommandList>
+      </CommandDialog>
+    </>
+  );
+}
diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx
new file mode 100644
index 0000000..1176ec8
--- /dev/null
+++ b/src/components/ui/command.tsx
@@ -0,0 +1,173 @@
+'use client';
+
+import { type DialogProps } from '@radix-ui/react-dialog';
+import { Command as CommandPrimitive } from 'cmdk';
+import { Search } from 'lucide-react';
+import * as React from 'react';
+
+import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog';
+import { cn } from '@/lib/utils';
+import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
+
+const Command = React.forwardRef<
+  React.ElementRef<typeof CommandPrimitive>,
+  React.ComponentPropsWithoutRef<typeof CommandPrimitive>
+>(({ className, ...props }, ref) => (
+  <CommandPrimitive
+    ref={ref}
+    className={cn(
+      'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',
+      className
+    )}
+    {...props}
+  />
+));
+Command.displayName = CommandPrimitive.displayName;
+
+const CommandDialog = ({
+  children,
+  commandValue,
+  onCommandValueChange,
+  commandFilter,
+  ...props
+}: DialogProps & {
+  commandValue?: string | undefined;
+  onCommandValueChange?: (value: string | undefined) => void;
+  commandFilter?: (value: string, input: string | undefined) => number;
+}) => {
+  return (
+    <Dialog {...props}>
+      <DialogContent className="overflow-hidden p-0">
+        <VisuallyHidden>
+          <DialogTitle>대화 상자 제목</DialogTitle>
+        </VisuallyHidden>
+        <Command
+          loop
+          value={commandValue}
+          onValueChange={onCommandValueChange}
+          filter={commandFilter}
+          className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"
+        >
+          {children}
+        </Command>
+      </DialogContent>
+    </Dialog>
+  );
+};
+
+const CommandInput = React.forwardRef<
+  React.ElementRef<typeof CommandPrimitive.Input>,
+  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
+>(({ className, ...props }, ref) => (
+  <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
+    <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
+    <CommandPrimitive.Input
+      ref={ref}
+      className={cn(
+        'flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
+        className
+      )}
+      {...props}
+    />
+  </div>
+));
+
+CommandInput.displayName = CommandPrimitive.Input.displayName;
+
+const CommandList = React.forwardRef<
+  React.ElementRef<typeof CommandPrimitive.List>,
+  React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
+>(({ className, ...props }, ref) => (
+  <CommandPrimitive.List
+    ref={ref}
+    className={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)}
+    {...props}
+  />
+));
+
+CommandList.displayName = CommandPrimitive.List.displayName;
+
+const CommandEmpty = React.forwardRef<
+  React.ElementRef<typeof CommandPrimitive.Empty>,
+  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
+>((props, ref) => (
+  <CommandPrimitive.Empty
+    ref={ref}
+    className="py-6 text-center text-sm"
+    {...props}
+  />
+));
+
+CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
+
+const CommandGroup = React.forwardRef<
+  React.ElementRef<typeof CommandPrimitive.Group>,
+  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
+>(({ className, ...props }, ref) => (
+  <CommandPrimitive.Group
+    ref={ref}
+    className={cn(
+      'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
+      className
+    )}
+    {...props}
+  />
+));
+
+CommandGroup.displayName = CommandPrimitive.Group.displayName;
+
+const CommandSeparator = React.forwardRef<
+  React.ElementRef<typeof CommandPrimitive.Separator>,
+  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
+>(({ className, ...props }, ref) => (
+  <CommandPrimitive.Separator
+    ref={ref}
+    className={cn('-mx-1 h-px bg-border', className)}
+    {...props}
+  />
+));
+CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
+
+const CommandItem = React.forwardRef<
+  React.ElementRef<typeof CommandPrimitive.Item>,
+  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
+>(({ className, ...props }, ref) => (
+  <CommandPrimitive.Item
+    ref={ref}
+    className={cn(
+      'relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
+      className
+    )}
+    {...props}
+  />
+));
+
+CommandItem.displayName = CommandPrimitive.Item.displayName;
+
+const CommandShortcut = ({
+  className,
+  ...props
+}: React.HTMLAttributes<HTMLSpanElement>) => {
+  return (
+    <span
+      className={cn(
+        'ml-auto text-xs tracking-widest text-muted-foreground',
+        className
+      )}
+      {...props}
+    />
+  );
+};
+CommandShortcut.displayName = 'CommandShortcut';
+
+export {
+  Command,
+  CommandDialog,
+  CommandEmpty,
+  CommandGroup,
+  CommandInput,
+  CommandItem,
+  CommandList,
+  CommandSeparator,
+  CommandShortcut,
+};
diff --git a/src/hooks/notion/useNotionIntegrationStatus.ts b/src/hooks/notion/useNotionIntegrationStatus.ts
new file mode 100644
index 0000000..229ecf2
--- /dev/null
+++ b/src/hooks/notion/useNotionIntegrationStatus.ts
@@ -0,0 +1,28 @@
+'use client';
+
+import { useSettingStore } from '@/store/settingStore';
+import Cookies from 'js-cookie';
+import { useEffect, useState } from 'react';
+
+export default function useNotionIntegrationStatus() {
+  const [notionApiKey, setNotionApiKey] = useState<string | undefined>();
+  const [openAIApiKey, setOpenAIApiKey] = useState<string | undefined>();
+  const [notionPageId] = useSettingStore((state) => state.notionPageId);
+  const [isNotionIntegrated, setIsNotionIntegrated] = useState<boolean>(false);
+  const [isOpenAIIntegrated, setIsOpenAIIntegrated] = useState<boolean>(false);
+
+  useEffect(() => {
+    setNotionApiKey(Cookies.get('notionApiKey'));
+    setOpenAIApiKey(Cookies.get('openAiApiKey'));
+  }, []);
+
+  useEffect(() => {
+    setIsNotionIntegrated(!!notionApiKey && !!notionPageId);
+    setIsOpenAIIntegrated(!!openAIApiKey);
+  }, [notionApiKey, openAIApiKey, notionPageId]);
+
+  return {
+    isNotionIntegrated,
+    isOpenAIIntegrated,
+  };
+}
diff --git a/src/hooks/utils/useDebounce.ts b/src/hooks/utils/useDebounce.ts
new file mode 100644
index 0000000..0aefc86
--- /dev/null
+++ b/src/hooks/utils/useDebounce.ts
@@ -0,0 +1,12 @@
+import { useEffect, useState } from 'react';
+
+export default function useDebounce<T>(value: T, delay: number): T {
+  const [debouncedValue, setDebouncedValue] = useState(value);
+
+  useEffect(() => {
+    const handler = setTimeout(() => setDebouncedValue(value), delay);
+    return () => clearTimeout(handler);
+  }, [value, delay]);
+
+  return debouncedValue;
+}
diff --git a/src/queries/notion.queries.ts b/src/queries/notion.queries.ts
index 51c1041..928d316 100644
--- a/src/queries/notion.queries.ts
+++ b/src/queries/notion.queries.ts
@@ -2,7 +2,11 @@
 /* eslint-disable @typescript-eslint/no-unsafe-return */
 
 import { useNotionStore } from '@/store/notionStore';
-import { NotionPagesResponse } from '@/types/dto/notion.dto.types';
+import {
+  NotionPagesResponse,
+  SearchRequestBody,
+  SearchResponse,
+} from '@/types/dto/notion.dto.types';
 import {
   GetBlockResponse,
   GetPageResponse,
@@ -102,3 +106,24 @@ export function useNotionBlock(blockId: string) {
     },
   });
 }
+
+// 노션 검색하기
+export function useNotionSearch() {
+  return useMutation<SearchResponse, Error, SearchRequestBody>({
+    mutationFn: async ({ query, filterType, pageSize }: SearchRequestBody) => {
+      const response = await fetch('/api/notion/search', {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({ query, filterType, pageSize }),
+      });
+
+      if (!response.ok) {
+        throw new Error('검색 중 오류가 발생했습니다.');
+      }
+
+      return response.json();
+    },
+  });
+}
diff --git a/src/types/dto/notion.dto.types.ts b/src/types/dto/notion.dto.types.ts
index 3f54000..dffedd4 100644
--- a/src/types/dto/notion.dto.types.ts
+++ b/src/types/dto/notion.dto.types.ts
@@ -4,3 +4,27 @@ export type NotionPagesResponse = {
   pages: NotionPageHierarchy;
   list: { pageId: string; pageTitle: string }[];
 };
+
+export type SearchRequestBody = {
+  query: string;
+  filterType: 'page' | 'database';
+  pageSize?: number;
+};
+
+export type SearchResponse = {
+  results: Array<{
+    [x: string]: any;
+    id: string;
+    title: string;
+    url: string;
+    properties: {
+      title: {
+        title: {
+          plain_text: string;
+        }[];
+      };
+    };
+  }>;
+  next_cursor: string | null;
+  has_more: boolean;
+};


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 4**

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>src/hooks/notion/useNotionIntegrationStatus.ts (1)</summary><blockquote>

`7-28`: **Notion 통합 상태 관리 훅 구현이 잘 되었습니다**

Notion 및 OpenAI 통합 상태를 관리하는 훅이 React 모범 사례에 따라 잘 구현되었습니다. 상태 관리와 부수 효과가 적절히 분리되어 있고, 의존성 배열이 올바르게 사용되었습니다.

다만, 쿠키 접근 시 오류 처리 로직을 추가하는 것이 좋을 수 있습니다:


```diff
- useEffect(() => {
-   setNotionApiKey(Cookies.get('notionApiKey'));
-   setOpenAIApiKey(Cookies.get('openAiApiKey'));
- }, []);
+ useEffect(() => {
+   try {
+     setNotionApiKey(Cookies.get('notionApiKey'));
+     setOpenAIApiKey(Cookies.get('openAiApiKey'));
+   } catch (error) {
+     console.error('쿠키 접근 중 오류 발생:', error);
+     // 필요한 경우 사용자에게 알림
+   }
+ }, []);
```

</blockquote></details>
<details>
<summary>src/app/api/notion/search/route.ts (1)</summary><blockquote>

`11-17`: **API 키 검증 부재**

API 요청을 처리하기 전에 `notionApiKey`가 쿠키에 존재하는지 확인하지 않고 있습니다. 키가 없는 경우 Notion 클라이언트 초기화가 실패할 수 있습니다.

API 키 존재 여부를 검증하고 적절한 오류 응답을 반환하는 코드를 추가하세요:

```diff
  try {
    const { query, filterType, pageSize } =
      (await req.json()) as SearchRequestBody;
    const cookies = parse(req.headers.get('cookie') || '');
    const notionApiKey = cookies.notionApiKey;
+    
+    if (!notionApiKey) {
+      return NextResponse.json(
+        { error: 'Notion API 키가 필요합니다. 설정에서 API 키를 추가해주세요.' },
+        { status: 401 }
+      );
+    }
    
    const notion = new Client({ auth: notionApiKey });
```

</blockquote></details>
<details>
<summary>src/components/notion/NotionSearchCommand.tsx (1)</summary><blockquote>

`87-103`: **라우팅 구현에 중복이 있습니다**

Button에 `onClick` 핸들러로 라우팅을 구현하고, 동시에 `asChild`와 함께 `Link` 컴포넌트를 사용하고 있어 중복이 발생합니다. 이는 불필요한 이벤트 핸들링과 잠재적인 성능 문제를 야기할 수 있습니다.

다음과 같이 두 가지 방법 중 하나만 사용하는 것이 좋습니다:

**방법 1: Link만 사용하기**
```diff
  <Button
    key={result.id}
    variant="ghost"
    className="w-full justify-start"
-   onClick={() => {
-     router.push(`/dashboard/notion/page?pageId=${result.id}`);
-   }}
    asChild
  >
    <Link href={`/dashboard/notion/page?pageId=${result.id}`}>
      <p className="text-left">
        {result.properties?.title.title[0].plain_text}
      </p>
    </Link>
  </Button>
```

**방법 2: onClick만 사용하기**
```diff
  <Button
    key={result.id}
    variant="ghost"
    className="w-full justify-start"
    onClick={() => {
      router.push(`/dashboard/notion/page?pageId=${result.id}`);
    }}
-   asChild
  >
-   <Link href={`/dashboard/notion/page?pageId=${result.id}`}>
      <p className="text-left">
        {result.properties?.title.title[0].plain_text}
      </p>
-   </Link>
  </Button>
```

</blockquote></details>
<details>
<summary>src/components/ui/command.tsx (1)</summary><blockquote>

`27-56`: **CommandDialog 컴포넌트의 인터페이스가 유연하게 설계되었습니다.**

`CommandDialog` 컴포넌트가 `commandValue`, `onCommandValueChange`, `commandFilter` 등 다양한 프로퍼티를 받아 유연한 인터페이스를 제공합니다. 이는 Notion 검색 기능에서 필요한 다양한 상황에 대응할 수 있도록 해줍니다.

다만, DialogTitle이 "대화 상자 제목"으로 고정되어 있어 다양한 사용 맥락에 맞게 커스터마이즈하기 어려울 수 있습니다.


다음과 같이 title을 props로 받아 유연하게 사용할 수 있도록 개선하는 것을 고려해보세요:

```diff
const CommandDialog = ({
  children,
  commandValue,
  onCommandValueChange,
  commandFilter,
+  title = "대화 상자 제목",
  ...props
}: DialogProps & {
  commandValue?: string | undefined;
  onCommandValueChange?: (value: string | undefined) => void;
  commandFilter?: (value: string, input: string | undefined) => number;
+  title?: string;
}) => {
  return (
    <Dialog {...props}>
      <DialogContent className="overflow-hidden p-0">
        <VisuallyHidden>
-         <DialogTitle>대화 상자 제목</DialogTitle>
+         <DialogTitle>{title}</DialogTitle>
        </VisuallyHidden>
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 32b6c90b5c483fde614023f0571a12c65c1e3ad4 and 60bc4ae144215f6ef1e30a0016c5c80277c9beff.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `package-lock.json` is excluded by `!**/package-lock.json`

</details>

<details>
<summary>📒 Files selected for processing (10)</summary>

* `package.json` (1 hunks)
* `src/app/api/notion/search/route.ts` (1 hunks)
* `src/components/dashboard/IntegrationStatus.tsx` (2 hunks)
* `src/components/layouts/Header.tsx` (2 hunks)
* `src/components/notion/NotionSearchCommand.tsx` (1 hunks)
* `src/components/ui/command.tsx` (1 hunks)
* `src/hooks/notion/useNotionIntegrationStatus.ts` (1 hunks)
* `src/hooks/utils/useDebounce.ts` (1 hunks)
* `src/queries/notion.queries.ts` (2 hunks)
* `src/types/dto/notion.dto.types.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (4)</summary>

<details>
<summary>src/app/api/notion/search/route.ts (1)</summary><blockquote>

<details>
<summary>src/types/dto/notion.dto.types.ts (1)</summary>

* `SearchRequestBody` (8-12)

</details>

</blockquote></details>
<details>
<summary>src/components/layouts/Header.tsx (3)</summary><blockquote>

<details>
<summary>src/hooks/notion/useNotionIntegrationStatus.ts (1)</summary>

* `useNotionIntegrationStatus` (7-28)

</details>
<details>
<summary>src/components/ui/navigation-menu.tsx (6)</summary>

* `NavigationMenu` (120-120)
* `NavigationMenuList` (121-121)
* `NavigationMenuItem` (122-122)
* `NavigationMenuTrigger` (124-124)
* `NavigationMenuContent` (123-123)
* `NavigationMenuLink` (125-125)

</details>
<details>
<summary>src/components/notion/NotionSearchCommand.tsx (1)</summary>

* `NotionSearchCommand` (18-128)

</details>

</blockquote></details>
<details>
<summary>src/components/dashboard/IntegrationStatus.tsx (1)</summary><blockquote>

<details>
<summary>src/hooks/notion/useNotionIntegrationStatus.ts (1)</summary>

* `useNotionIntegrationStatus` (7-28)

</details>

</blockquote></details>
<details>
<summary>src/queries/notion.queries.ts (1)</summary><blockquote>

<details>
<summary>src/types/dto/notion.dto.types.ts (2)</summary>

* `SearchResponse` (14-30)
* `SearchRequestBody` (8-12)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (18)</summary><blockquote>

<details>
<summary>package.json (1)</summary>

`33-33`: **새로운 UI 컴포넌트 종속성 추가 확인**

새로 추가된 두 종속성(`@radix-ui/react-visually-hidden` 및 `cmdk`)은 PR 목표인 Notion 문서 검색 기능 구현을 위한 UI 컴포넌트에 필요한 라이브러리입니다. 이 종속성들은 명령 대화창 인터페이스와 접근성 향상에 기여할 것으로 보입니다.



Also applies to: 40-40

</details>
<details>
<summary>src/hooks/utils/useDebounce.ts (1)</summary>

`3-12`: **검색 최적화를 위한 디바운스 훅 구현이 잘 되었습니다**

제네릭 타입을 사용하여 다양한 값 유형에 재사용 가능한 디바운스 훅이 잘 구현되었습니다. PR 목표에 언급된 대로 검색 입력에 300ms 디바운스를 적용하여 API 호출 빈도를 90% 줄이는 데 기여할 것으로 보입니다.

특히 다음 사항이 잘 처리되었습니다:
- 제네릭 타입을 통한 타입 안전성 확보
- useEffect의 정리 함수에서 타임아웃 해제로 메모리 누수 방지
- 적절한 의존성 배열 사용

</details>
<details>
<summary>src/types/dto/notion.dto.types.ts (1)</summary>

`8-12`: **검색 요청을 위한 타입 정의가 명확합니다**

`SearchRequestBody` 타입이 검색 기능에 필요한 매개변수(쿼리, 필터 타입, 페이지 크기)를 명확하게 정의하고 있습니다. 특히 `filterType`에 'page'와 'database'만 허용하도록 제한한 것이 좋습니다.

</details>
<details>
<summary>src/queries/notion.queries.ts (1)</summary>

`110-129`: **코드가 잘 구현되었습니다!**

`useNotionSearch` 함수는 기존 파일의 패턴을 잘 따르고 있으며, 에러 처리도 적절히 구현되어 있습니다. `useMutation` 훅을 사용하여 검색 요청을 처리하는 접근 방식이 적절합니다.

</details>
<details>
<summary>src/components/notion/NotionSearchCommand.tsx (1)</summary>

`25-47`: **디바운스 구현이 우수합니다**

검색 입력에 대해 디바운스를 적용하여 불필요한 API 호출을 줄이는 접근 방식이 아주 좋습니다. 키보드 단축키 구현도 사용자 경험을 향상시키는 좋은 기능입니다.

</details>
<details>
<summary>src/components/dashboard/IntegrationStatus.tsx (2)</summary>

`4-10`: **커스텀 훅으로 코드가 개선되었습니다!**

`useNotionIntegrationStatus` 훅을 사용하여 로직을 분리한 것은 좋은 리팩토링입니다. 이렇게 하면 코드가 더 간결해지고 관심사 분리(Separation of Concerns)가 잘 이루어져 유지보수성이 향상됩니다.

---

`21-34`: **상태에 따른 UI 업데이트가 잘 구현되었습니다**

통합 상태에 따라 UI를 조건부로 렌더링하는 부분이 깔끔하게 구현되었습니다. 불필요한 로직이 제거되고 커스텀 훅의 값을 직접 활용하여 코드가 더 명확해졌습니다.

</details>
<details>
<summary>src/components/layouts/Header.tsx (5)</summary>

`3-21`: **새로운 UI 요소와 훅들이 적절하게 임포트되었습니다.**

새로운 기능을 지원하기 위해 필요한 컴포넌트들이 잘 임포트되었습니다. 특히 `NotionSearchCommand` 컴포넌트와 네비게이션 메뉴 관련 컴포넌트들은 반응형 UI를 구현하는 데 중요한 역할을 할 것으로 보입니다.

---

`26-27`: **Notion 통합 상태와 모바일 화면 여부를 적절하게 확인하고 있습니다.**

`useIsMobile`과 `useNotionIntegrationStatus` 훅을 사용하여 UI 렌더링 조건을 설정하는 방식이 적절합니다. 이를 통해 디바이스 타입과 Notion 통합 상태에 따라 동적으로 UI를 조정할 수 있습니다.

---

`59-72`: **데스크톱 화면에서의 네비게이션 메뉴 구현이 잘 되었습니다.**

데스크톱 화면(`!isMobile`)에서 가로형 네비게이션을 제공하는 방식이 깔끔하게 구현되었습니다. 현재 경로에 따라 active 상태를 시각적으로 표시하는 조건부 스타일링도 사용자 경험을 향상시킵니다.

---

`74-99`: **모바일 화면에서의 드롭다운 메뉴 구현이 적절합니다.**

모바일 화면에서 `NavigationMenu` 컴포넌트를 사용하여 드롭다운 형태의 메뉴를 제공하는 방식이 공간 효율적입니다. 이렇게 함으로써 모바일 화면에서도 사용자가 모든 메뉴에 쉽게 접근할 수 있게 되었습니다.

---

`100-100`: **Notion 검색 기능이 조건부로 적절하게 렌더링됩니다.**

`isNotionIntegrated` 상태에 따라 `NotionSearchCommand` 컴포넌트를 조건부로 렌더링하는 방식이 적절합니다. 이를 통해 Notion이 통합되지 않은 사용자에게는 불필요한 UI 요소를 보여주지 않게 됩니다.

</details>
<details>
<summary>src/components/ui/command.tsx (6)</summary>

`1-11`: **필요한 의존성과 컴포넌트들이 잘 임포트되었습니다.**

명령어 대화창(Command Dialog) 구현을 위한 필수 요소들이 적절하게 임포트되었습니다. 특히 `cmdk` 라이브러리를 활용한 접근 방식은 커맨드 팔레트 패턴을 효과적으로 구현할 수 있게 해줍니다.

---

`12-25`: **기본 Command 컴포넌트가 잘 구현되었습니다.**

`CommandPrimitive`를 확장한 `Command` 컴포넌트가 `forwardRef`를 활용하여 적절히 구현되었습니다. 이로써 ref 전달이 가능해지며 다양한 UI 상황에서 유연하게 활용될 수 있습니다.

---

`58-75`: **CommandInput 컴포넌트가 검색 아이콘과 함께 잘 구현되었습니다.**

`CommandInput` 컴포넌트가 `Search` 아이콘과 함께 시각적으로 직관적인 입력 필드를 제공합니다. `forwardRef`를 통한 ref 전달 및 클래스 관리가 깔끔하게 구현되어 있습니다.

---

`77-89`: **CommandList 컴포넌트의 스크롤 처리가 적절합니다.**

`CommandList` 컴포넌트에 `max-h-[300px]`와 `overflow-y-auto` 스타일이 적용되어, 검색 결과가 많을 경우에도 UI가 깨지지 않고 스크롤을 통해 탐색할 수 있게 구현되었습니다.

---

`90-161`: **다양한 Command 관련 컴포넌트들이 일관된 패턴으로 구현되었습니다.**

`CommandEmpty`, `CommandGroup`, `CommandSeparator`, `CommandItem`, `CommandShortcut` 등 다양한 컴포넌트들이 일관된 패턴으로 구현되어 있어 코드의 가독성과 유지보수성이 높습니다. 특히 모든 컴포넌트에 `displayName`을 명시적으로 설정한 것은 디버깅 시 컴포넌트 식별을 용이하게 해줍니다.

---

`163-173`: **모든 컴포넌트가 적절히 export되었습니다.**

구현된 모든 Command 관련 컴포넌트들이 명시적으로 export되어 있어, 다른 파일에서 필요에 따라 개별 컴포넌트를 import하여 사용할 수 있습니다. 이는 코드의 모듈성과 재사용성을 높여줍니다.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-25T10:56:03Z
user: login: wonsik3686
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-25T11:37:15Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-25T11:37:37Z

Review comments:
diff_hunk: @@ -4,3 +4,27 @@ export type NotionPagesResponse = {
   pages: NotionPageHierarchy;
   list: { pageId: string; pageTitle: string }[];
 };
+
+export type SearchRequestBody = {
+  query: string;
+  filterType: 'page' | 'database';
+  pageSize?: number;
+};
+
+export type SearchResponse = {
+  results: Array<{
+    [x: string]: any;
+    id: string;
+    title: string;
+    url: string;
+    properties: {
+      title: {
+        title: {
+          plain_text: string;
+        }[];
+      };
+    };
+  }>;
+  next_cursor: string | null;
+  has_more: boolean;
+};
path: src/types/dto/notion.dto.types.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**검색 응답 타입에서 타입 안전성 개선 가능**

`SearchResponse` 타입이 Notion API 응답 구조를 잘 반영하고 있습니다만, 몇 가지 개선할 점이 있습니다:

1. 16번 줄의 `[x: string]: any`는 타입 안전성을 약화시킵니다. 가능하다면 더 구체적인 타입으로 정의하는 것이 좋습니다.
2. `properties.title.title` 구조가 다소 중복적으로 보입니다. 가능하다면 명확한 네이밍으로 개선할 수 있습니다.


```diff
export type SearchResponse = {
  results: Array<{
-   [x: string]: any;
+   // Notion API에서 제공하는 알려진 속성들만 명시적으로 정의
+   object: string;
+   // 기타 알려진 속성들...
    id: string;
    title: string;
    url: string;
    properties: {
      title: {
-       title: {
+       content: {  // 더 명확한 네이밍 제안
          plain_text: string;
        }[];
      };
    };
  }>;
  next_cursor: string | null;
  has_more: boolean;
};
```

아래 스크립트를 실행하여 Notion API 응답 구조를 확인해 보세요:

---

🏁 Script executed:

```shell
#!/bin/bash
# Notion API 응답 구조를 확인하기 위한 스크립트
# 프로젝트에서 API 호출이나 타입 사용 부분 찾기

# API 호출 부분 찾기
echo "Notion API 검색 엔드포인트 구현 확인:"
rg -A 3 "api/notion/search" --type ts

# 타입 사용 부분 찾기
echo -e "\nSearchResponse 타입 사용 확인:"
rg -A 3 "SearchResponse" --type ts
```

Length of output: 1252

---



</details>

**타입 안정성 강화 및 네이밍 개선 필요**

현재 `SearchResponse` 타입은 Notion API 응답 구조를 대체로 잘 반영하고 있으나, 다음과 같이 개선할 부분이 있습니다:

- **타입 안전성 강화:**  
  객체 내에 `[x: string]: any;`를 사용하는 것은 너무 포괄적이어서 불필요한 타입 위험 요소가 될 수 있습니다. Notion API에서 반환하는 알려진 속성(예, `object` 등)을 명시적으로 정의하여 사용하는 것이 좋습니다.

- **네이밍 개선:**  
  `properties.title.title`과 같이 중복된 네이밍 대신, 가독성을 고려한 명확한 이름(예, `properties.title.content`)으로 변경하는 것을 제안합니다.

예시 변경안:
```diff
 export type SearchResponse = {
   results: Array<{
-    [x: string]: any;
+    // Notion API에서 제공하는 알려진 속성들만 명시적으로 정의
+    object: string;
+    // 필요한 다른 속성 추가
     id: string;
     title: string;
     url: string;
     properties: {
       title: {
-        title: {
+        content: {  // 명확한 네이밍 예시
          plain_text: string;
        }[];
      };
    };
  }>;
  next_cursor: string | null;
  has_more: boolean;
};
```

이와 같이 수정할 경우, 타입 안전성이 개선되고 코드의 가독성도 크게 향상될 것으로 기대됩니다.



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
export type SearchResponse = {
  results: Array<{
    // Notion API에서 제공하는 알려진 속성들만 명시적으로 정의
    object: string;
    // 기타 알려진 속성들...
    id: string;
    title: string;
    url: string;
    properties: {
      title: {
        content: {  // 더 명확한 네이밍 제안
          plain_text: string;
        }[];
      };
    };
  }>;
  next_cursor: string | null;
  has_more: boolean;
};
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-25T10:56:02Z
author_association: NONE
diff_hunk: @@ -0,0 +1,39 @@
+import { Client } from '@notionhq/client';
+import { parse } from 'cookie';
+import { NextRequest, NextResponse } from 'next/server';
+
+type SearchRequestBody = {
+  query: string;
+  filterType: 'page' | 'database';
+  pageSize?: number;
+};
path: src/app/api/notion/search/route.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**타입 중복 정의 문제**

이 파일에서 `SearchRequestBody` 타입을 정의하고 있지만, 동일한 타입이 이미 `src/types/dto/notion.dto.types.ts`에 정의되어 있습니다. 중복 정의는 코드 유지보수를 어렵게 만들 수 있습니다.

타입 정의를 제거하고 import를 추가하는 것이 좋겠습니다:

```diff
import { Client } from '@notionhq/client';
import { parse } from 'cookie';
import { NextRequest, NextResponse } from 'next/server';
+import { SearchRequestBody } from '@/types/dto/notion.dto.types';

-type SearchRequestBody = {
-  query: string;
-  filterType: 'page' | 'database';
-  pageSize?: number;
-};
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
import { Client } from '@notionhq/client';
import { parse } from 'cookie';
import { NextRequest, NextResponse } from 'next/server';
import { SearchRequestBody } from '@/types/dto/notion.dto.types';
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-25T10:56:02Z
author_association: NONE
diff_hunk: @@ -0,0 +1,39 @@
+import { Client } from '@notionhq/client';
+import { parse } from 'cookie';
+import { NextRequest, NextResponse } from 'next/server';
+
+type SearchRequestBody = {
+  query: string;
+  filterType: 'page' | 'database';
+  pageSize?: number;
+};
+
+export async function POST(req: NextRequest) {
+  try {
+    const { query, filterType, pageSize } =
+      (await req.json()) as SearchRequestBody;
+    const cookies = parse(req.headers.get('cookie') || '');
+    const notionApiKey = cookies.notionApiKey;
+    const notion = new Client({ auth: notionApiKey });
+
+    const response = await notion.search({
+      query,
+      filter: {
+        value: filterType,
+        property: 'object',
+      },
+      sort: {
+        direction: 'ascending',
+        timestamp: 'last_edited_time',
+      },
+      page_size: pageSize || 100,
+    });
+
+    return NextResponse.json(response);
+  } catch (error) {
+    return NextResponse.json(
+      { error: '검색 중 오류가 발생했습니다.', errorMessage: error },
+      { status: 500 }
+    );
path: src/app/api/notion/search/route.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**에러 응답에서 민감한 정보 노출**

에러 응답에 `errorMessage: error` 형태로 원본 에러 객체를 그대로 포함시키고 있습니다. 이는 잠재적으로 민감한 정보(API 키, 내부 서버 세부 정보 등)가 클라이언트에 노출될 수 있는 보안 위험이 있습니다.

에러 메시지만 안전하게 반환하도록 수정하세요:

```diff
    return NextResponse.json(
-      { error: '검색 중 오류가 발생했습니다.', errorMessage: error },
+      { error: '검색 중 오류가 발생했습니다.' },
      { status: 500 }
    );
```

서버 로그에 에러를 기록하고 싶다면 다음과 같이 구현할 수 있습니다:
```diff
  } catch (error) {
+    console.error('Notion 검색 에러:', error);
    return NextResponse.json(
-      { error: '검색 중 오류가 발생했습니다.', errorMessage: error },
+      { error: '검색 중 오류가 발생했습니다.' },
      { status: 500 }
    );
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
// General error response (if applicable)
return NextResponse.json(
  { error: '검색 중 오류가 발생했습니다.' },
  { status: 500 }
);

// Within a catch block for error handling
} catch (error) {
  console.error('Notion 검색 에러:', error);
  return NextResponse.json(
    { error: '검색 중 오류가 발생했습니다.' },
    { status: 500 }
  );
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-25T10:56:03Z
author_association: NONE
diff_hunk: @@ -0,0 +1,128 @@
+'use client';
+
+import { Loader2, Search } from 'lucide-react';
+
+import {
+  CommandDialog,
+  CommandInput,
+  CommandList,
+} from '@/components/ui/command';
+import { useIsMobile } from '@/hooks/shadcn/use-mobile';
+import useDebounce from '@/hooks/utils/useDebounce';
+import { useNotionSearch } from '@/queries/notion.queries';
+import Link from 'next/link';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { Button } from '../ui/button';
+
+export default function NotionSearchCommand() {
+  const [open, setOpen] = useState(false);
+  const [searchQuery, setSearchQuery] = useState('');
+  const isMobile = useIsMobile();
+  const router = useRouter();
+  const { mutate: searchNotion, data, isError, isPending } = useNotionSearch();
+
+  const debouncedSearchQuery = useDebounce(searchQuery, 300);
+
+  useEffect(() => {
+    const down = (e: KeyboardEvent) => {
+      if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
+        e.preventDefault();
+        setOpen((open) => !open);
+      }
+    };
+
+    document.addEventListener('keydown', down);
+    return () => document.removeEventListener('keydown', down);
+  }, []);
+
+  useEffect(() => {
+    if (debouncedSearchQuery.trim()) {
+      handleSearch(debouncedSearchQuery);
+    }
+  }, [debouncedSearchQuery]);
+
+  const handleSearch = (value: string) => {
+    searchNotion({ query: value, filterType: 'page', pageSize: 100 });
+  };
+
+  return (
+    <>
+      {isMobile ? (
+        <Button variant="outline" size="icon" onClick={() => setOpen(true)}>
+          <Search className="h-4 w-4" />
+        </Button>
+      ) : (
+        <Button
+          className="w-full justify-start rounded-lg px-4 py-1 text-left font-normal text-muted-foreground hover:text-foreground"
+          variant="outline"
+          size="icon"
+          onClick={() => setOpen(true)}
+        >
+          <p className="flex w-48 items-center justify-between text-sm">
+            <span className="flex items-center gap-1 text-xs">
+              <Search className="h-4 w-4" />
+              Notion 페이지 검색..
+            </span>
+            <kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
+              <span className="text-xs">⌘</span>K
+            </kbd>
+          </p>
+        </Button>
+      )}
+      <CommandDialog
+        open={open}
+        onOpenChange={setOpen}
+        commandValue={searchQuery}
+      >
+        <CommandInput
+          placeholder="페이지 제목 검색..."
+          onValueChange={(value) => {
+            setSearchQuery(value);
+          }}
+        />
+
+        <CommandList>
+          {data?.results.map((result) => {
+            return (
+              <Button
+                key={result.id}
+                variant="ghost"
+                className="w-full justify-start"
+                onClick={() => {
+                  router.push(`/dashboard/notion/page?pageId=${result.id}`);
+                }}
+                asChild
+              >
+                <Link href={`/dashboard/notion/page?pageId=${result.id}`}>
+                  <p className="text-left">
+                    {result.properties?.title.title[0].plain_text}
+                  </p>
+                </Link>
path: src/components/notion/NotionSearchCommand.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**속성 안전성 검증 필요**

검색 결과에서 `result.properties?.title.title[0].plain_text`를 접근할 때 optional chaining이 부분적으로만 적용되어 있습니다. `title` 배열이 비어있거나 존재하지 않을 경우 런타임 오류가 발생할 수 있습니다.

모든 프로퍼티에 대해 안전하게 접근하도록 수정하세요:

```diff
  <p className="text-left">
-   {result.properties?.title.title[0].plain_text}
+   {result.properties?.title?.title?.[0]?.plain_text || '제목 없음'}
  </p>
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
                <Link href={`/dashboard/notion/page?pageId=${result.id}`}>
                  <p className="text-left">
                    {result.properties?.title?.title?.[0]?.plain_text || '제목 없음'}
                  </p>
                </Link>
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-03-25T10:56:03Z
author_association: NONE
diff_hunk: @@ -0,0 +1,128 @@
+'use client';
+
+import { Loader2, Search } from 'lucide-react';
+
+import {
+  CommandDialog,
+  CommandInput,
+  CommandList,
+} from '@/components/ui/command';
+import { useIsMobile } from '@/hooks/shadcn/use-mobile';
+import useDebounce from '@/hooks/utils/useDebounce';
+import { useNotionSearch } from '@/queries/notion.queries';
+import Link from 'next/link';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { Button } from '../ui/button';
+
+export default function NotionSearchCommand() {
+  const [open, setOpen] = useState(false);
+  const [searchQuery, setSearchQuery] = useState('');
+  const isMobile = useIsMobile();
+  const router = useRouter();
+  const { mutate: searchNotion, data, isError, isPending } = useNotionSearch();
+
+  const debouncedSearchQuery = useDebounce(searchQuery, 300);
+
+  useEffect(() => {
+    const down = (e: KeyboardEvent) => {
+      if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
+        e.preventDefault();
+        setOpen((open) => !open);
+      }
+    };
+
+    document.addEventListener('keydown', down);
+    return () => document.removeEventListener('keydown', down);
+  }, []);
+
+  useEffect(() => {
+    if (debouncedSearchQuery.trim()) {
+      handleSearch(debouncedSearchQuery);
+    }
+  }, [debouncedSearchQuery]);
+
+  const handleSearch = (value: string) => {
+    searchNotion({ query: value, filterType: 'page', pageSize: 100 });
+  };
+
+  return (
+    <>
+      {isMobile ? (
+        <Button variant="outline" size="icon" onClick={() => setOpen(true)}>
+          <Search className="h-4 w-4" />
+        </Button>
+      ) : (
+        <Button
+          className="w-full justify-start rounded-lg px-4 py-1 text-left font-normal text-muted-foreground hover:text-foreground"
+          variant="outline"
+          size="icon"
+          onClick={() => setOpen(true)}
+        >
+          <p className="flex w-48 items-center justify-between text-sm">
+            <span className="flex items-center gap-1 text-xs">
+              <Search className="h-4 w-4" />
+              Notion 페이지 검색..
+            </span>
+            <kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
+              <span className="text-xs">⌘</span>K
+            </kbd>
+          </p>
+        </Button>
+      )}
+      <CommandDialog
+        open={open}
+        onOpenChange={setOpen}
+        commandValue={searchQuery}
+      >
+        <CommandInput
+          placeholder="페이지 제목 검색..."
+          onValueChange={(value) => {
+            setSearchQuery(value);
+          }}
+        />
+
+        <CommandList>
+          {data?.results.map((result) => {
+            return (
+              <Button
+                key={result.id}
+                variant="ghost"
+                className="w-full justify-start"
+                onClick={() => {
+                  router.push(`/dashboard/notion/page?pageId=${result.id}`);
+                }}
+                asChild
+              >
+                <Link href={`/dashboard/notion/page?pageId=${result.id}`}>
+                  <p className="text-left">
+                    {result.properties?.title.title[0].plain_text}
+                  </p>
+                </Link>
path: src/components/notion/NotionSearchCommand.tsx
user: login: wonsik3686
body: 수정 완료했습니다
created_at: 2025-03-25T11:37:15Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,128 @@
+'use client';
+
+import { Loader2, Search } from 'lucide-react';
+
+import {
+  CommandDialog,
+  CommandInput,
+  CommandList,
+} from '@/components/ui/command';
+import { useIsMobile } from '@/hooks/shadcn/use-mobile';
+import useDebounce from '@/hooks/utils/useDebounce';
+import { useNotionSearch } from '@/queries/notion.queries';
+import Link from 'next/link';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { Button } from '../ui/button';
+
+export default function NotionSearchCommand() {
+  const [open, setOpen] = useState(false);
+  const [searchQuery, setSearchQuery] = useState('');
+  const isMobile = useIsMobile();
+  const router = useRouter();
+  const { mutate: searchNotion, data, isError, isPending } = useNotionSearch();
+
+  const debouncedSearchQuery = useDebounce(searchQuery, 300);
+
+  useEffect(() => {
+    const down = (e: KeyboardEvent) => {
+      if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
+        e.preventDefault();
+        setOpen((open) => !open);
+      }
+    };
+
+    document.addEventListener('keydown', down);
+    return () => document.removeEventListener('keydown', down);
+  }, []);
+
+  useEffect(() => {
+    if (debouncedSearchQuery.trim()) {
+      handleSearch(debouncedSearchQuery);
+    }
+  }, [debouncedSearchQuery]);
+
+  const handleSearch = (value: string) => {
+    searchNotion({ query: value, filterType: 'page', pageSize: 100 });
+  };
+
+  return (
+    <>
+      {isMobile ? (
+        <Button variant="outline" size="icon" onClick={() => setOpen(true)}>
+          <Search className="h-4 w-4" />
+        </Button>
+      ) : (
+        <Button
+          className="w-full justify-start rounded-lg px-4 py-1 text-left font-normal text-muted-foreground hover:text-foreground"
+          variant="outline"
+          size="icon"
+          onClick={() => setOpen(true)}
+        >
+          <p className="flex w-48 items-center justify-between text-sm">
+            <span className="flex items-center gap-1 text-xs">
+              <Search className="h-4 w-4" />
+              Notion 페이지 검색..
+            </span>
+            <kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
+              <span className="text-xs">⌘</span>K
+            </kbd>
+          </p>
+        </Button>
+      )}
+      <CommandDialog
+        open={open}
+        onOpenChange={setOpen}
+        commandValue={searchQuery}
+      >
+        <CommandInput
+          placeholder="페이지 제목 검색..."
+          onValueChange={(value) => {
+            setSearchQuery(value);
+          }}
+        />
+
+        <CommandList>
+          {data?.results.map((result) => {
+            return (
+              <Button
+                key={result.id}
+                variant="ghost"
+                className="w-full justify-start"
+                onClick={() => {
+                  router.push(`/dashboard/notion/page?pageId=${result.id}`);
+                }}
+                asChild
+              >
+                <Link href={`/dashboard/notion/page?pageId=${result.id}`}>
+                  <p className="text-left">
+                    {result.properties?.title.title[0].plain_text}
+                  </p>
+                </Link>
path: src/components/notion/NotionSearchCommand.tsx
user: login: coderabbitai[bot]
body: @wonsik3686 좋습니다! 코드 수정을 완료해주셔서 감사합니다. 이제 속성 접근이 안전하게 처리되어 런타임 오류를 방지할 수 있게 되었습니다. 👍

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-03-25T11:37:37Z
author_association: NONE

PR comments:
user: login: vercel[bot]
created_at: 2025-03-25T10:50:12Z
body: [vc]: #uvZVBhRkpFXvU1yKiwQtsObFCDMUrHv7Q2GqVRSrqFI=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJnZW5pZS1kb2NzIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImdlbmllLWRvY3MtZ2l0LWZlYXR1cmUtMjctd29uc2lrcy1wcm9qZWN0cy52ZXJjZWwuYXBwIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS93b25zaWtzLXByb2plY3RzL2dlbmllLWRvY3MvOEZCb2RaQWoxQWNoSFdoRFpXZDNCdUs5SlZNMyIsInByZXZpZXdVcmwiOiJnZW5pZS1kb2NzLWdpdC1mZWF0dXJlLTI3LXdvbnNpa3MtcHJvamVjdHMudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsInJvb3REaXJlY3RvcnkiOm51bGx9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **genie-docs** | ✅ Ready ([Inspect](https://vercel.com/wonsiks-projects/genie-docs/8FBodZAj1AchHWhDZWd3BuK9JVM3)) | [Visit Preview](https://genie-docs-git-feature-27-wonsiks-projects.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/genie-docs-git-feature-27-wonsiks-projects.vercel.app?via=pr-comment-feedback-link) | Mar 25, 2025 11:38am |


author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-03-25T10:50:15Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
이번 PR은 여러 부분에서 변경이 이루어졌습니다.  
- `package.json`에 두 개의 새 의존성이 추가되었습니다.  
- Notion API를 활용한 새로운 문서 검색 API 엔드포인트와 관련 쿼리 기능, DTO 타입이 도입되었습니다.  
- Notion 검색 UI 컴포넌트와 명령 대화창 등이 새로 추가되었습니다.  
- 통합 상태를 관리하는 로직과 헤더 컴포넌트, 대시보드의 상태 관리가 개선 및 리팩토링되었습니다.  
- 또한, 유틸리티 목적으로 디바운스 훅이 추가되었습니다.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `package.json` | 새 의존성 `@radix-ui/react-visually-hidden` (^1.1.2) 및 `cmdk` (^1.1.1) 추가 |
| `src/app/api/.../route.ts`, `src/queries/notion.queries.ts`, `src/types/dto/...notion.dto.types.ts` | Notion 검색 API 엔드포인트 추가, 검색 요청/응답을 위한 쿼리 기능 및 DTO 타입 도입 |
| `src/components/notion/NotionSearchCommand.tsx`, `src/components/ui/command.tsx` | Notion 검색 UI 및 명령 대화창 컴포넌트 추가, 검색 입력 및 결과 렌더링 로직 구현 |
| `src/components/dashboard/IntegrationStatus.tsx`, `src/components/layouts/Header.tsx`, `src/hooks/notion/useNotionIntegrationStatus.ts` | Notion 및 OpenAI 통합 상태 관리 로직 개선 및 통합, 헤더의 반응형 UI 개선 |
| `src/hooks/utils/useDebounce.ts` | 일반적인 디바운스 훅 추가하여 검색 입력의 요청 빈도 제어 |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant U as 사용자
    participant NSC as NotionSearchCommand
    participant DB as Debounce Hook
    participant NS as useNotionSearch
    participant API as /api/notion/search
    participant NAPI as Notion API

    U->>NSC: 검색 쿼리 입력
    NSC->>DB: 쿼리 전송 (debounce)
    DB-->>NSC: 디바운스된 쿼리 전달
    NSC->>NS: 검색 요청
    NS->>API: POST 요청 (query, filterType, pageSize)
    API->>NAPI: Notion 검색 호출
    NAPI-->>API: 검색 결과 응답
    API-->>NS: JSON 결과 전달
    NS-->>NSC: 결과 업데이트
    NSC->>U: 결과 렌더링
```

## Assessment against linked issues

| Objective                                  | Addressed | Explanation  |
|--------------------------------------------|:---------:|--------------|
| [#27] 문서 검색 기능 구현 (문서 검색창, 실시간 검색) | ✅       |              |

## Poem
> 코딩 숲속을 달리는 나, 귀여운 토끼  
> 변경된 코드들이 반짝반짝 빛나네  
> Notion API와 함께 춤을 추며  
> 문서를 찾는 그 기쁨, 잊지 못할 모험  
> 🐇✨ 코드의 세계에서 자유롭게 뛰놀아요!

<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 60bc4ae144215f6ef1e30a0016c5c80277c9beff and 79f7f976269f6055ad4ccb62cbd74562d16c2709.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `src/components/notion/NotionSearchCommand.tsx` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* src/components/notion/NotionSearchCommand.tsx

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=wonsik3686/genie-docs&utm_content=28):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE