Skip to content

[feat/search-95] ✨ feat: 검색 기능 추가#96

Merged
swallowedB merged 3 commits intodevfrom
feat/search-95
Jan 13, 2026
Merged

[feat/search-95] ✨ feat: 검색 기능 추가#96
swallowedB merged 3 commits intodevfrom
feat/search-95

Conversation

@swallowedB
Copy link
Owner

@swallowedB swallowedB commented Jan 13, 2026

요약

  • 변경 목적(왜?): 사이트 내 이동성과 탐색성을 높이고, 글/페이지/메타데이터 기반 검색 기능을 도입하기 위함
  • 주요 변경(무엇을?): 커맨드 팔레트 검색 기능 구현, Velite 기반 포스트 검색 연동, 단축키/상태관리 분리, UI 개선 및 텍스트 수정

변경 내용

  • UI/컴포넌트
    • CommandPaletteProvider 구축 및 Dialog(UI) 분리
    • Search 버튼(헤더/히어로)에서 openPalette() 호출 연동
    • CmdK 입력 UI 및 리스트 스타일링 적용
    • Placeholder/힌트 문구 동화적 톤으로 개선
  • 로직/유틸
    • Velite getAllPosts() 기반 search index 구성
    • title/summary/tags/category/slug 대상 검색 매칭 구현 (대소문자 무시)
    • useCommandPaletteInternal 네이밍 변경 및 로직 정돈
    • open/close/toggle/ESC/⌘K 단축키 처리
    • 모달 닫힘 시 query 초기화 처리
  • 문서/설정
    • (해당 없음)

스크린샷/동영상 (선택)

스크린샷 2026-01-13 오후 10 21 03

테스트

  • 유닛 테스트 추가/수정됨
  • 로컬에서 pnpm test 통과 (해당 없음)
  • 타입체크/린트 통과 (pnpm typecheck, pnpm lint)

관련 이슈

close #95

Summary by CodeRabbit

  • 새로운 기능
    • 명령 팔레트 추가 (⌘K 단축키) — 검색 가능한 인터페이스로 콘텐츠 및 포스트 빠르게 탐색
  • 변경 사항
    • 홈 검색 UI를 정적 검색바에서 명령 팔레트 트리거 버튼으로 변경
    • 헤더 검색 버튼에 반응형 애니메이션 및 키보드 힌트 추가
    • 모바일 메뉴에 부드러운 애니메이션 적용 및 내비게이션 동작 개선
    • 홈 소개 문구 업데이트

✏️ Tip: You can customize this high-level summary in your review settings.

@swallowedB swallowedB linked an issue Jan 13, 2026 that may be closed by this pull request
3 tasks
@vercel
Copy link

vercel bot commented Jan 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
b0o0a Ready Ready Preview, Comment Jan 13, 2026 1:30pm

@coderabbitai
Copy link

coderabbitai bot commented Jan 13, 2026

📝 Walkthrough

Walkthrough

글로벌 Command Palette(⌘K/Ctrl+K) 기반 전역 검색 기능을 추가하고, 이를 레이아웃 루트에 Provider로 통합했습니다. 기존 홈 검색 UI는 명령 팔레트 트리거 버튼으로 교체되었고 검색 표시/모바일 메뉴 로직은 별도 훅으로 분리되었습니다.

Changes

코호트 / 파일(s) 변경 요약
의존성 추가
package.json
@radix-ui/react-dialog@^1.1.15, cmdk@^1.1.1 추가
Command Palette 구현
src/components/common/CommandPalette.tsx,
src/hooks/useCommandPaletteInternal.ts
새로운 Command Palette 컴포넌트, Provider, useCommandPalette 훅 및 내부 상태 훅 추가(검색 쿼리, 필터링, 선택·내비게이션, ⌘K 단축키)
레이아웃 통합
src/app/(layout)/layout.tsx
전체 레이아웃을 CommandPaletteProvider로 래핑
홈 컴포넌트 변경
src/app/(layout)/(shell)/_components/home/HeroLeft.tsx
기존 SearchBar 제거, Command Palette를 여는 버튼으로 대체 (openPalette 사용)
홈 태그 목록 제거
src/app/(layout)/(shell)/_components/home/HeroTagList.tsx
파일 삭제 — 태그 버튼 렌더링 컴포넌트 제거
검색/메뉴 전환 훅 추가
src/app/(layout)/(shell)/_components/home/_hooks/useSearchTransition.ts
홈 페이지 스크롤 기반 검색 표시/숨김, 모바일 메뉴 토글 및 handleSearchClick(openPalette 호출) 제공
헤더 리팩터 & 애니메이션
src/app/(layout)/(shell)/_components/layout/SiteHeader.tsx
useSearchTransition 통합, AnimatePresence/motion 사용해 검색 버튼·모바일 메뉴 애니메이션 및 접근성 힌트 추가
상수 변경
src/app/(layout)/(shell)/_constants/home.ts
HERO_BRANDING, HERO_POPULAR_TAGS 제거, HERO_INTRO 문자열 내용 변경
테스트 삭제
src/app/(layout)/(shell)/_components/layout/_tests_/*
SiteHeader.test.tsx, SiteFooter.test.tsx 삭제

Sequence Diagram

sequenceDiagram
    participant User
    participant Header as SiteHeader
    participant Provider as CommandPaletteProvider
    participant Palette as CommandPalette (Cmdk)
    participant Dialog as Radix Dialog
    participant Router as Next.js Router

    User->>Header: ⌘K 누르거나 검색 버튼 클릭
    Header->>Provider: openPalette() 호출
    Provider->>Dialog: open 상태 활성화
    Dialog->>Palette: 명령 팔레트 렌더링 (입력 포커스)
    User->>Palette: 쿼리 입력
    Palette->>Palette: 필터링 (label/hint/searchableText)
    Palette->>User: 필터된 결과 표시
    User->>Palette: 항목 선택(엔터/클릭)
    Palette->>Router: 선택된 item.href로 네비게이트
    Palette->>Dialog: Dialog 닫기 (query 리셋)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🔎 ⌘K가 문을 두드리면, 작은 모달이 빛나네.
키 입력은 바다처럼 필터를 가르고,
페이지와 글이 손을 흔들어 나타나네.
한 번의 단축키로 길이 열리고,
작은 버튼이 큰 탐험을 부른다. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 제목은 커맨드 팔레트 기반 검색 기능 도입이라는 주요 변경을 명확히 나타냅니다.
Description check ✅ Passed PR 설명이 변경 목적, 주요 변경 내용(UI/로직), 스크린샷, 테스트 상태, 관련 이슈를 포함하여 템플릿을 잘 따릅니다.
Linked Issues check ✅ Passed PR은 #95의 커맨드 팔레트 기반 검색 기능 도입, 모달 UI, 키보드 단축키(⌘K/Ctrl+K) 지원 요구사항을 모두 구현했습니다.
Out of Scope Changes check ✅ Passed 패키지 의존성 추가(@radix-ui/react-dialog, cmdk), 컴포넌트 변경(HeroLeft, SiteHeader), 훅 추가, 테스트 파일 삭제, 상수 수정이 모두 검색 기능 구현에 필요한 범위 내 변경입니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3628297 and beca858.

📒 Files selected for processing (2)
  • src/app/(layout)/(shell)/_components/layout/_tests_/SiteFooter.test.tsx
  • src/app/(layout)/(shell)/_components/layout/_tests_/SiteHeader.test.tsx
💤 Files with no reviewable changes (2)
  • src/app/(layout)/(shell)/_components/layout/tests/SiteFooter.test.tsx
  • src/app/(layout)/(shell)/_components/layout/tests/SiteHeader.test.tsx

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In @src/app/(layout)/(shell)/_components/home/_hooks/useSearchTransition.ts:
- Around line 40-42: handleSearchClick is just a thin wrapper that only calls
openPalette; remove the unnecessary wrapper by either returning/exporting
openPalette directly from the hook or replacing the function body with a direct
assignment (e.g., const handleSearchClick = openPalette) and update all callers
to use openPalette (or the reassigned handleSearchClick) accordingly; adjust any
tests or usages that referenced handleSearchClick to the chosen exported symbol
to keep behavior unchanged.

In @src/app/(layout)/(shell)/_components/layout/SiteHeader.tsx:
- Around line 89-98: The aria-label on the mobile menu button is static; update
the motion.button inside the SiteHeader component to set aria-label dynamically
based on isMobileMenuOpen (e.g., use "모바일 메뉴 닫기" when true and "모바일 메뉴 열기" when
false) and also add aria-expanded={isMobileMenuOpen} to reflect state; the
change should be applied to the existing motion.button that calls
toggleMobileMenu and renders {isMobileMenuOpen ? <X .../> : <Menu .../>}.
- Around line 10-17: Tests render SiteHeader without the CommandPaletteProvider
causing useSearchTransition (which calls useCommandPalette) to fail; wrap
SiteHeader in CommandPaletteProvider in SiteHeader.test.tsx around each render
(e.g., the render in the "로고가 렌더링된다" test and other tests) so the context is
available. Also update the mobile menu toggle button in SiteHeader (the element
using isMobileMenuOpen) to set its aria-label dynamically using isMobileMenuOpen
so it reads "모바일 메뉴 닫기" when open and "모바일 메뉴 열기" when closed instead of a fixed
string.

In @src/components/common/CommandPalette.tsx:
- Around line 62-65: Add a Dialog.Description to the CommandPalette Dialog to
improve screen-reader context: inside the Dialog.Content near the existing
Dialog.Title (which is sr-only), insert a Dialog.Description with a brief
descriptive string (e.g., a short Korean sentence like "검색어를 입력하여 사이트 내 항목을
찾습니다") and mark it sr-only if you want it hidden visually, so assistive tech
reads both title and description; ensure you import/use the same Dialog
namespace (Dialog.Description) so the Radix Dialog properly associates the
description with the dialog.
- Around line 81-103: The Command component is performing its own filtering in
addition to the custom filtering from useCommandPaletteInternal, causing
double-filtering; update the Command (root) element to disable built-in
filtering by adding the shouldFilter={false} prop so only filteredItems from
useCommandPaletteInternal are rendered (ensure you reference the Command.List
that maps filteredItems and keep Command.Item rendering unchanged).
- Around line 9-15: Command.Item currently passes only item.label as the value
prop, which prevents the generated searchableText from being used by cmdk's
filtering; update the value usage so it becomes value={item.searchableText ||
item.label} on Command.Item (or alternatively disable cmdk's built-in filtering
by adding shouldFilter={false} to the Command container) to ensure
searchableText is honored — look for Command.Item in CommandPalette.tsx and
adjust the value prop or add shouldFilter on the Command component.

In @src/hooks/useCommandPaletteInternal.ts:
- Around line 142-155: The handler inside useEffect uses the deprecated
navigator.platform to detect macOS; update the platform detection in the handler
(used by useEffect and togglePalette) to use feature detection: prefer
navigator.userAgentData?.platform for modern browsers and fall back to
navigator.platform or a userAgent sniff only if userAgentData is unavailable;
ensure the logic still derives the boolean isMac and uses it to compute meta
(e.metaKey vs e.ctrlKey) and keep existing prevention and togglePalette
behavior.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6ea0de1 and 3628297.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • package.json
  • src/app/(layout)/(shell)/_components/home/HeroLeft.tsx
  • src/app/(layout)/(shell)/_components/home/HeroTagList.tsx
  • src/app/(layout)/(shell)/_components/home/_hooks/useSearchTransition.ts
  • src/app/(layout)/(shell)/_components/layout/SiteHeader.tsx
  • src/app/(layout)/(shell)/_constants/home.ts
  • src/app/(layout)/layout.tsx
  • src/components/common/CommandPalette.tsx
  • src/hooks/useCommandPaletteInternal.ts
💤 Files with no reviewable changes (1)
  • src/app/(layout)/(shell)/_components/home/HeroTagList.tsx
🧰 Additional context used
🧬 Code graph analysis (5)
src/app/(layout)/(shell)/_components/home/_hooks/useSearchTransition.ts (1)
src/components/common/CommandPalette.tsx (1)
  • useCommandPalette (31-39)
src/app/(layout)/(shell)/_components/layout/SiteHeader.tsx (2)
src/app/(layout)/(shell)/_components/home/_hooks/useSearchTransition.ts (1)
  • useSearchTransition (7-52)
src/components/ui/DarkModeWheel.tsx (1)
  • DarkModeWheel (9-75)
src/app/(layout)/(shell)/_components/home/HeroLeft.tsx (1)
src/components/common/CommandPalette.tsx (1)
  • useCommandPalette (31-39)
src/hooks/useCommandPaletteInternal.ts (2)
src/components/common/CommandPalette.tsx (1)
  • CommandItem (9-15)
src/lib/posts/queries.ts (1)
  • getAllPosts (9-17)
src/components/common/CommandPalette.tsx (1)
src/hooks/useCommandPaletteInternal.ts (1)
  • useCommandPaletteInternal (75-169)
🪛 GitHub Actions: CI
src/app/(layout)/(shell)/_components/layout/SiteHeader.tsx

[error] 17-17: Test failed: useCommandPalette must be used within CommandPaletteProvider.

src/components/common/CommandPalette.tsx

[error] 34-34: useCommandPalette must be used within CommandPaletteProvider.

🔇 Additional comments (14)
src/app/(layout)/(shell)/_constants/home.ts (1)

1-2: LGTM!

상수 텍스트 업데이트입니다. 문제없습니다.

src/app/(layout)/layout.tsx (1)

5-12: LGTM!

CommandPaletteProvider가 레이아웃 최상위에 적절히 배치되었습니다. 하위 컴포넌트들이 useCommandPalette 훅을 사용할 수 있습니다.

src/hooks/useCommandPaletteInternal.ts (2)

41-73: LGTM!

buildPostItems가 포스트 데이터를 검색 가능한 형태로 잘 변환합니다. searchableText에 title, summary, tags, category, series, slug를 모두 포함하여 다양한 검색이 가능합니다.


80-83: 정적 포스트 데이터에 대한 빈 의존성 배열

itemsuseMemo 의존성 배열이 비어 있어 포스트가 런타임에 변경되어도 업데이트되지 않습니다. getAllPosts가 정적 데이터를 반환하므로 현재는 문제없지만, 이 의도가 맞는지 확인해 주세요.

src/app/(layout)/(shell)/_components/home/HeroLeft.tsx (2)

11-12: LGTM!

useCommandPalette 훅에서 openPalette만 필요한 만큼만 destructure하여 사용하고 있습니다.


28-65: 검색 버튼 UI 구현이 잘 되었습니다

버튼이 type="button"으로 명시되어 있고, 라이트/다크 모드에 대한 스타일링이 잘 적용되었습니다. ⌘K 힌트도 적절하게 표시됩니다.

src/app/(layout)/(shell)/_components/home/_hooks/useSearchTransition.ts (1)

14-29: 스크롤 이벤트 리스너 구현이 적절합니다

홈 페이지에서만 스크롤 리스너를 등록하고, cleanup 함수로 잘 정리하고 있습니다. 다만, 홈에서 다른 페이지로 이동 시 pastThreshold 상태가 true로 남아있을 수 있습니다. 다시 홈으로 돌아왔을 때 handleScroll()이 초기 호출되므로 실질적 문제는 없지만, 명시적으로 리셋하면 더 명확할 수 있습니다.

src/app/(layout)/(shell)/_components/layout/SiteHeader.tsx (2)

27-54: LGTM!

AnimatePresence를 사용한 검색 버튼의 진입/퇴장 애니메이션이 잘 구현되어 있습니다. showSearch 상태에 따라 조건부 렌더링이 적절하게 동작합니다.


102-133: 모바일 메뉴 애니메이션 구현이 적절합니다

AnimatePresencemotion.div를 사용하여 모바일 메뉴의 열림/닫힘 애니메이션이 부드럽게 처리됩니다. 네비게이션 링크 클릭 시 closeMobileMenu를 호출하여 메뉴가 자동으로 닫히는 것도 좋은 UX입니다.

package.json (1)

20-23: 새 의존성 추가 적절함

@radix-ui/react-dialogcmdk는 Command Palette 구현에 잘 맞는 조합입니다. cmdk가 내부적으로 @radix-ui/react-dialog에 의존하고 있으므로, 버전 관리가 명확합니다. 두 라이브러리 모두 최신 버전을 사용하고 있어 문제없습니다.

src/components/common/CommandPalette.tsx (4)

41-59: LGTM!

Provider 구현이 깔끔합니다. 내부 상태(open, query, filteredItems)는 Provider 내부에만 유지하고, 외부에는 필요한 액션(openPalette, closePalette, togglePalette)만 노출하는 좋은 캡슐화입니다.


106-110: LGTM!

Dialog와 Provider 구조가 올바르게 구성되어 있습니다. children이 Context Provider 내부에 렌더링되어 하위 컴포넌트들이 useCommandPalette 훅에 접근할 수 있습니다.


31-39: 원본 코드의 에러 처리는 올바르게 구현되어 있습니다.

useCommandPalette 훅의 컨텍스트 확인 로직(31-39번 라인)은 정상입니다. 코드 검토 결과:

  • CommandPaletteProvidersrc/app/(layout)/layout.tsx에서 HeroLeftuseSearchTransition을 포함한 모든 자식 컴포넌트를 감싸고 있습니다.
  • useCommandPalette를 사용하는 모든 컴포넌트가 provider 범위 내에 있으므로, 제시된 에러가 발생할 조건이 없습니다.

CI 파이프라인 실패 메시지에 대한 구체적인 증거가 필요합니다. 현재 코드 구조상 정상 작동해야 하는 상황이므로, 실제 에러 로그나 재현 가능한 시나리오가 있는지 확인해 주세요.

Likely an incorrect or invalid review comment.


62-63: z-index 커스텀 값 확인 필요

z-60z-70은 Tailwind CSS의 기본 유틸리티가 아닙니다. Tailwind의 기본 z-index 유틸리티는 z-0, z-10, z-20, z-30, z-40, z-50까지만 제공됩니다. 이 값들이 프로젝트의 tailwind.config에 커스텀으로 정의되어 있는지 확인해 주세요. 정의되지 않았다면 스타일이 제대로 적용되지 않을 수 있습니다.

Comment on lines +40 to +42
const handleSearchClick = () => {
openPalette();
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

handleSearchClickopenPalette의 단순 래퍼입니다

현재 handleSearchClickopenPalette()만 호출합니다. 향후 analytics 등 추가 로직이 필요하다면 이 구조가 유용하지만, 그렇지 않다면 openPalette를 직접 반환하는 것도 고려해 볼 수 있습니다.

🤖 Prompt for AI Agents
In @src/app/(layout)/(shell)/_components/home/_hooks/useSearchTransition.ts
around lines 40 - 42, handleSearchClick is just a thin wrapper that only calls
openPalette; remove the unnecessary wrapper by either returning/exporting
openPalette directly from the hook or replacing the function body with a direct
assignment (e.g., const handleSearchClick = openPalette) and update all callers
to use openPalette (or the reassigned handleSearchClick) accordingly; adjust any
tests or usages that referenced handleSearchClick to the chosen exported symbol
to keep behavior unchanged.

Comment on lines 10 to +17
export default function SiteHeader() {
const [open, setOpen] = useState(false);
const {
showSearch,
isMobileMenuOpen,
toggleMobileMenu,
closeMobileMenu,
handleSearchClick,
} = useSearchTransition();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if the file exists and read relevant sections
echo "=== Checking SiteHeader.tsx file ==="
if [ -f "src/app/(layout)/(shell)/_components/layout/SiteHeader.tsx" ]; then
  wc -l "src/app/(layout)/(shell)/_components/layout/SiteHeader.tsx"
  echo ""
  echo "=== Lines 10-17 ==="
  sed -n '10,17p' "src/app/(layout)/(shell)/_components/layout/SiteHeader.tsx"
  echo ""
  echo "=== Lines 90-95 (around line 93) ==="
  sed -n '90,95p' "src/app/(layout)/(shell)/_components/layout/SiteHeader.tsx"
else
  echo "File not found at src/app/(layout)/(shell)/_components/layout/SiteHeader.tsx"
  # Try to find it
  fd "SiteHeader.tsx"
fi

Repository: swallowedB/b-log

Length of output: 630


🏁 Script executed:

#!/bin/bash
# Find test files related to SiteHeader
echo "=== Finding test files ==="
fd -type f \( -name "*SiteHeader*" -o -name "*site-header*" \) | head -20
echo ""
echo "=== Finding all test files in the directory ==="
fd -path "*shell*" -type f \( -name "*.test.tsx" -o -name "*.spec.tsx" -o -name "*.test.ts" -o -name "*.spec.ts" \) | head -20

Repository: swallowedB/b-log

Length of output: 492


🏁 Script executed:

#!/bin/bash
# Search for CommandPaletteProvider in the codebase
echo "=== Searching for CommandPaletteProvider usage ==="
rg "CommandPaletteProvider" -n | head -30

Repository: swallowedB/b-log

Length of output: 697


🏁 Script executed:

#!/bin/bash
# Find and examine the useSearchTransition hook
echo "=== Finding useSearchTransition hook ==="
fd -name "*.ts" -o -name "*.tsx" | xargs rg -l "useSearchTransition" | head -5

Repository: swallowedB/b-log

Length of output: 479


🏁 Script executed:

#!/bin/bash
# Check the layout.tsx file to see if SiteHeader is rendered within CommandPaletteProvider
echo "=== Checking layout.tsx ==="
cat -n src/app/\(layout\)/layout.tsx | head -40

Repository: swallowedB/b-log

Length of output: 572


🏁 Script executed:

#!/bin/bash
# Read the useSearchTransition hook implementation
echo "=== Reading useSearchTransition hook ==="
cat -n src/app/\(layout\)/\(shell\)/_components/home/_hooks/useSearchTransition.ts

Repository: swallowedB/b-log

Length of output: 1652


🏁 Script executed:

#!/bin/bash
# Search for test files more broadly
echo "=== Searching for test files ==="
find . -type f \( -name "*.test.ts*" -o -name "*.spec.ts*" \) | grep -i "header\|shell" | head -20

Repository: swallowedB/b-log

Length of output: 240


🏁 Script executed:

#!/bin/bash
# Read the SiteHeader component fully to understand context
echo "=== Reading full SiteHeader.tsx ==="
cat -n src/app/\(layout\)/\(shell\)/_components/layout/SiteHeader.tsx

Repository: swallowedB/b-log

Length of output: 6045


🏁 Script executed:

#!/bin/bash
# Read the SiteHeader test file to check if it wraps with CommandPaletteProvider
echo "=== Reading SiteHeader.test.tsx ==="
cat -n src/app/\(layout\)/\(shell\)/_components/layout/_tests_/SiteHeader.test.tsx

Repository: swallowedB/b-log

Length of output: 1407


🏁 Script executed:

#!/bin/bash
# Check CommandPalette implementation to understand the error
echo "=== Reading CommandPalette.tsx context setup ==="
head -50 src/components/common/CommandPalette.tsx | cat -n

Repository: swallowedB/b-log

Length of output: 1552


테스트에서 CommandPaletteProvider로 감싸기 필요

SiteHeader.test.tsx를 확인하니 현재 CommandPaletteProvider 없이 컴포넌트를 렌더링하고 있습니다(14줄). 하지만 useSearchTransition 훅이 내부적으로 useCommandPalette()를 호출하므로(useSearchTransition.ts 10줄), 프로바이더 컨텍스트 없이 실행되면 오류가 발생합니다.

테스트를 다음과 같이 수정해 주세요:

describe("SiteHeader", () => {
  test("로고가 렌더링된다", () => {
    render(
      <CommandPaletteProvider>
        <SiteHeader />
      </CommandPaletteProvider>
    );
    // ...
  });
  // 다른 테스트도 동일하게 적용
});

모바일 메뉴 토글 버튼의 aria-label 수정

93줄의 aria-label이 "모바일 메뉴 열기"라고 고정되어 있는데, 실제로는 토글 버튼입니다(메뉴를 열거나 닫음). 접근성을 위해 현재 상태를 반영하도록 수정하세요:

aria-label={isMobileMenuOpen ? "모바일 메뉴 닫기" : "모바일 메뉴 열기"}
🧰 Tools
🪛 GitHub Actions: CI

[error] 17-17: Test failed: useCommandPalette must be used within CommandPaletteProvider.

🤖 Prompt for AI Agents
In @src/app/(layout)/(shell)/_components/layout/SiteHeader.tsx around lines 10 -
17, Tests render SiteHeader without the CommandPaletteProvider causing
useSearchTransition (which calls useCommandPalette) to fail; wrap SiteHeader in
CommandPaletteProvider in SiteHeader.test.tsx around each render (e.g., the
render in the "로고가 렌더링된다" test and other tests) so the context is available.
Also update the mobile menu toggle button in SiteHeader (the element using
isMobileMenuOpen) to set its aria-label dynamically using isMobileMenuOpen so it
reads "모바일 메뉴 닫기" when open and "모바일 메뉴 열기" when closed instead of a fixed
string.

Comment on lines +89 to +98
<motion.button
type="button"
className="md:hidden"
onClick={() => setOpen(!open)}
onClick={toggleMobileMenu}
aria-label="모바일 메뉴 열기"
whileTap={{ scale: 0.9, opacity: 0.8 }}
transition={{ duration: 0.08 }}
>
{open ? <X size={24} /> : <Menu size={24} />}
</button>
{isMobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
</motion.button>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

모바일 메뉴 토글 버튼의 aria-label 개선 제안

현재 aria-label="모바일 메뉴 열기"로 고정되어 있지만, 메뉴가 열려있을 때는 "닫기"가 더 적절합니다. 스크린 리더 사용자를 위해 동적으로 변경하는 것을 권장합니다.

♻️ 접근성 개선 제안
          <motion.button
            type="button"
            className="md:hidden"
            onClick={toggleMobileMenu}
-           aria-label="모바일 메뉴 열기"
+           aria-label={isMobileMenuOpen ? "모바일 메뉴 닫기" : "모바일 메뉴 열기"}
+           aria-expanded={isMobileMenuOpen}
            whileTap={{ scale: 0.9, opacity: 0.8 }}
            transition={{ duration: 0.08 }}
          >
            {isMobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
          </motion.button>
📝 Committable suggestion

‼️ 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.

Suggested change
<motion.button
type="button"
className="md:hidden"
onClick={() => setOpen(!open)}
onClick={toggleMobileMenu}
aria-label="모바일 메뉴 열기"
whileTap={{ scale: 0.9, opacity: 0.8 }}
transition={{ duration: 0.08 }}
>
{open ? <X size={24} /> : <Menu size={24} />}
</button>
{isMobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
</motion.button>
<motion.button
type="button"
className="md:hidden"
onClick={toggleMobileMenu}
aria-label={isMobileMenuOpen ? "모바일 메뉴 닫기" : "모바일 메뉴 열기"}
aria-expanded={isMobileMenuOpen}
whileTap={{ scale: 0.9, opacity: 0.8 }}
transition={{ duration: 0.08 }}
>
{isMobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
</motion.button>
🤖 Prompt for AI Agents
In @src/app/(layout)/(shell)/_components/layout/SiteHeader.tsx around lines 89 -
98, The aria-label on the mobile menu button is static; update the motion.button
inside the SiteHeader component to set aria-label dynamically based on
isMobileMenuOpen (e.g., use "모바일 메뉴 닫기" when true and "모바일 메뉴 열기" when false)
and also add aria-expanded={isMobileMenuOpen} to reflect state; the change
should be applied to the existing motion.button that calls toggleMobileMenu and
renders {isMobileMenuOpen ? <X .../> : <Menu .../>}.

Comment on lines +9 to +15
export type CommandItem = {
id: string;
label: string;
hint?: string;
href?: string;
searchableText?: string;
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n src/components/common/CommandPalette.tsx | head -100

Repository: swallowedB/b-log

Length of output: 4181


🏁 Script executed:

rg "searchableText" src/components/common/CommandPalette.tsx -B 2 -A 2

Repository: swallowedB/b-log

Length of output: 124


🌐 Web query:

cmdk library Command.Item value prop filtering behavior search

💡 Result:

Short answer — how Command.Item.value affects filtering in cmdk:

  • If you pass a value prop on Command.Item, that value (not the rendered text) is used by the built‑in filter as the item’s primary string. If you don’t pass value it’s inferred from the item’s textContent. [1][2]
  • The filter function is called with (value, search[, keywords]) and by default both strings are normalized (trim + lowercase) before comparing — unless you provide a custom filter. You can also provide keywords on Command.Item to act as aliases for filtering. [1][2]
  • You can disable built‑in filtering with shouldFilter={false} or supply a custom filter to control ranking/removal. [1][2]
  • Note: cmdk introduced a breaking change in v1.0 — the value prop became case‑sensitive (still trimmed). If you rely on automatic lowercasing, update code or use a custom filter. [3]

Sources: [1] cmdk README (GitHub). [2] cmdk docs on npm (older docs). [3] cmdk v1.0.0 release notes about value case sensitivity.


🏁 Script executed:

find . -name "*useCommandPalette*" -type f

Repository: swallowedB/b-log

Length of output: 100


🏁 Script executed:

rg "useCommandPaletteInternal" --type ts --type tsx -l

Repository: swallowedB/b-log

Length of output: 87


🏁 Script executed:

cat -n ./src/hooks/useCommandPaletteInternal.ts

Repository: swallowedB/b-log

Length of output: 5098


searchableText 필드를 Command.Itemvalue prop에 전달하세요

searchableText는 실제로 useCommandPaletteInternal에서 생성되고 있으며, 커스텀 필터링 로직에 포함되어 있습니다(label, hint와 함께). 다만 CommandPalette.tsx Line 90에서 Command.Itemvalue prop으로는 item.label만 전달되고 있습니다.

cmdk 라이브러리에서 value prop은 필터링의 기준이 되므로, 현재 구조에서는:

  • useCommandPaletteInternal의 커스텀 필터링: label + hint + searchableText 모두 사용 ✓
  • Command.Item의 value: label만 사용 ✗

이미 필터링된 filteredItems를 렌더링하므로 실제 버그는 아니지만, searchableText가 생성되는 의도를 완전히 활용하려면 value={item.searchableText || item.label}로 수정하거나, cmdk의 자동 필터링을 의도적으로 비활성화하려면 shouldFilter={false} 속성을 Command에 추가하는 것이 더 명확합니다.

🤖 Prompt for AI Agents
In @src/components/common/CommandPalette.tsx around lines 9 - 15, Command.Item
currently passes only item.label as the value prop, which prevents the generated
searchableText from being used by cmdk's filtering; update the value usage so it
becomes value={item.searchableText || item.label} on Command.Item (or
alternatively disable cmdk's built-in filtering by adding shouldFilter={false}
to the Command container) to ensure searchableText is honored — look for
Command.Item in CommandPalette.tsx and adjust the value prop or add shouldFilter
on the Command component.

Comment on lines +62 to +65
<Dialog.Overlay className="fixed inset-0 z-60 bg-black/40 backdrop-blur-sm" />
<Dialog.Content className="fixed left-1/2 top-50 z-70 w-full max-w-xl -translate-x-1/2 rounded-2xl border border-white/10 bg-foreground/70 dark:bg-foreground shadow-2xl">
<Dialog.Title className="sr-only">사이트 검색</Dialog.Title>

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

접근성 개선 제안: Dialog.Description 추가

Radix UI Dialog는 스크린 리더 사용자를 위해 Dialog.Description을 제공할 것을 권장합니다. 현재 Dialog.Titlesr-only로 잘 처리되어 있는데, Description도 추가하면 더 좋을 것 같습니다.

♻️ 제안 코드
 <Dialog.Title className="sr-only">사이트 검색</Dialog.Title>
+<Dialog.Description className="sr-only">
+  페이지, 포스트, 명령어를 검색하세요
+</Dialog.Description>
📝 Committable suggestion

‼️ 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.

Suggested change
<Dialog.Overlay className="fixed inset-0 z-60 bg-black/40 backdrop-blur-sm" />
<Dialog.Content className="fixed left-1/2 top-50 z-70 w-full max-w-xl -translate-x-1/2 rounded-2xl border border-white/10 bg-foreground/70 dark:bg-foreground shadow-2xl">
<Dialog.Title className="sr-only">사이트 검색</Dialog.Title>
<Dialog.Overlay className="fixed inset-0 z-60 bg-black/40 backdrop-blur-sm" />
<Dialog.Content className="fixed left-1/2 top-50 z-70 w-full max-w-xl -translate-x-1/2 rounded-2xl border border-white/10 bg-foreground/70 dark:bg-foreground shadow-2xl">
<Dialog.Title className="sr-only">사이트 검색</Dialog.Title>
<Dialog.Description className="sr-only">
페이지, 포스트, 명령어를 검색하세요
</Dialog.Description>
🤖 Prompt for AI Agents
In @src/components/common/CommandPalette.tsx around lines 62 - 65, Add a
Dialog.Description to the CommandPalette Dialog to improve screen-reader
context: inside the Dialog.Content near the existing Dialog.Title (which is
sr-only), insert a Dialog.Description with a brief descriptive string (e.g., a
short Korean sentence like "검색어를 입력하여 사이트 내 항목을 찾습니다") and mark it sr-only if
you want it hidden visually, so assistive tech reads both title and description;
ensure you import/use the same Dialog namespace (Dialog.Description) so the
Radix Dialog properly associates the description with the dialog.

Comment on lines +81 to +103
<Command.List className="max-h-80 overflow-y-auto py-2">
<Command.Empty className="px-4 py-3 text-xs text-background/70">
검색 결과가 없습니다.
</Command.Empty>

<Command.Group className="px-2 pt-1">
{filteredItems.map((item) => (
<Command.Item
key={item.id}
value={item.label}
onSelect={() => handleSelect(item)}
className="flex cursor-pointer flex-col gap-0.5 rounded-xl px-3 py-2 text-sm aria-selected:bg-background/10"
>
<span className="font-medium">{item.label}</span>
{item.hint && (
<span className="text-xs text-background/70">
{item.hint}
</span>
)}
</Command.Item>
))}
</Command.Group>
</Command.List>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

cmdk 필터링 동작 확인 필요

현재 filteredItemsuseCommandPaletteInternal에서 이미 필터링된 결과입니다. 그런데 cmdk의 Command 컴포넌트는 기본적으로 자체 필터링 로직을 가지고 있어서, 이중 필터링이 발생할 수 있습니다.

만약 커스텀 필터링만 사용하려면 Command 컴포넌트에 shouldFilter={false}를 추가하는 것이 좋습니다:

♻️ 제안 코드
-<Command className="overflow-hidden rounded-2xl bg-transparent text-sm text-background">
+<Command shouldFilter={false} className="overflow-hidden rounded-2xl bg-transparent text-sm text-background">

이렇게 하면 useCommandPaletteInternal의 필터링 로직만 사용되어 searchableText 필드를 포함한 커스텀 검색이 일관되게 동작합니다.

🤖 Prompt for AI Agents
In @src/components/common/CommandPalette.tsx around lines 81 - 103, The Command
component is performing its own filtering in addition to the custom filtering
from useCommandPaletteInternal, causing double-filtering; update the Command
(root) element to disable built-in filtering by adding the shouldFilter={false}
prop so only filteredItems from useCommandPaletteInternal are rendered (ensure
you reference the Command.List that maps filteredItems and keep Command.Item
rendering unchanged).

Comment on lines +142 to +155
useEffect(() => {
const handler = (e: KeyboardEvent) => {
const isMac = navigator.platform.toLowerCase().includes("mac");
const meta = isMac ? e.metaKey : e.ctrlKey;

if (meta && e.key.toLowerCase() === "k") {
e.preventDefault();
togglePalette();
}
};

window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [togglePalette]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

navigator.platform은 deprecated 상태입니다

navigator.platform은 deprecated되었으며, 향후 브라우저에서 제거될 수 있습니다. 현재로서는 동작에 문제가 없지만, 장기적으로 navigator.userAgentData를 사용하는 방안을 고려해 볼 수 있습니다.

♻️ 대안 제안 (optional)
  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
-     const isMac = navigator.platform.toLowerCase().includes("mac");
+     const isMac = navigator.userAgentData?.platform?.toLowerCase().includes("mac") 
+       ?? navigator.platform?.toLowerCase().includes("mac") 
+       ?? false;
      const meta = isMac ? e.metaKey : e.ctrlKey;

      if (meta && e.key.toLowerCase() === "k") {
        e.preventDefault();
        togglePalette();
      }
    };

    window.addEventListener("keydown", handler);
    return () => window.removeEventListener("keydown", handler);
  }, [togglePalette]);
🤖 Prompt for AI Agents
In @src/hooks/useCommandPaletteInternal.ts around lines 142 - 155, The handler
inside useEffect uses the deprecated navigator.platform to detect macOS; update
the platform detection in the handler (used by useEffect and togglePalette) to
use feature detection: prefer navigator.userAgentData?.platform for modern
browsers and fall back to navigator.platform or a userAgent sniff only if
userAgentData is unavailable; ensure the logic still derives the boolean isMac
and uses it to compute meta (e.metaKey vs e.ctrlKey) and keep existing
prevention and togglePalette behavior.

@swallowedB swallowedB merged commit c46753a into dev Jan 13, 2026
4 of 5 checks passed
@swallowedB swallowedB deleted the feat/search-95 branch January 13, 2026 13:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ feat: 검색 기능 추가

1 participant