feat(search): / 단축키 검색 모달 및 통합 검색 API 추가
- / 및 헤더 검색 클릭으로 모달을 열고 태그·게시물 검색을 제공. - 태그 검색 범위를 name/slug로 제한하고 IME 조합 입력 대응을 보강. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,6 +3,7 @@ const { menuOpen, toggleMenu, closeMenu } = useMenuState()
|
||||
const menuUserOpen = ref(false)
|
||||
const userMenuRef = ref(null)
|
||||
const userMenuToggleRef = ref(null)
|
||||
const searchOpen = ref(false)
|
||||
|
||||
const { data: siteSettings } = await useFetch('/api/site-settings', {
|
||||
default: () => ({
|
||||
@@ -18,6 +19,37 @@ const closeUserMenu = () => {
|
||||
menuUserOpen.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 통합 검색 모달을 연다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const openSearchModal = () => {
|
||||
searchOpen.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 입력 필드에 포커스가 있으면 `/` 검색 단축키를 무시한다.
|
||||
* @param {KeyboardEvent} event - 키보드 이벤트
|
||||
* @returns {boolean} 무시할 때 true
|
||||
*/
|
||||
const shouldIgnoreSearchHotkey = (event) => {
|
||||
if (event.ctrlKey || event.metaKey || event.altKey) {
|
||||
return true
|
||||
}
|
||||
const target = event.target
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return false
|
||||
}
|
||||
const tag = target.tagName
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') {
|
||||
return true
|
||||
}
|
||||
if (target.isContentEditable) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 메뉴를 토글한다.
|
||||
* @returns {void}
|
||||
@@ -47,24 +79,37 @@ const onDocumentClick = (event) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape로 열린 패널을 닫는다(사용자 메뉴 우선, 이어서 모바일 좌측 메뉴).
|
||||
* Escape·`/` 키로 패널을 제어한다(검색 모달 → 사용자 메뉴 → 모바일 좌측 메뉴, `/`는 검색 열기).
|
||||
* @param {KeyboardEvent} event - 키보드 이벤트
|
||||
* @returns {void}
|
||||
*/
|
||||
const onGlobalKeydown = (event) => {
|
||||
if (event.key !== 'Escape') {
|
||||
if (event.key === 'Escape') {
|
||||
if (searchOpen.value) {
|
||||
searchOpen.value = false
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
if (menuUserOpen.value) {
|
||||
closeUserMenu()
|
||||
return
|
||||
}
|
||||
if (!menuOpen.value) {
|
||||
return
|
||||
}
|
||||
if (typeof window !== 'undefined' && window.matchMedia('(max-width: 1023px)').matches) {
|
||||
closeMenu()
|
||||
}
|
||||
return
|
||||
}
|
||||
if (menuUserOpen.value) {
|
||||
closeUserMenu()
|
||||
if (event.key !== '/') {
|
||||
return
|
||||
}
|
||||
if (!menuOpen.value) {
|
||||
if (shouldIgnoreSearchHotkey(event)) {
|
||||
return
|
||||
}
|
||||
if (typeof window !== 'undefined' && window.matchMedia('(max-width: 1023px)').matches) {
|
||||
closeMenu()
|
||||
}
|
||||
event.preventDefault()
|
||||
openSearchModal()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -114,11 +159,16 @@ onBeforeUnmount(() => {
|
||||
</button>
|
||||
<span class="min-w-0 truncate">{{ siteSettings.title }}</span>
|
||||
</NuxtLink>
|
||||
<div class="site-header__search site-header__search--responsive hidden h-9 min-w-0 flex-1 basis-0 items-center rounded-lg px-3 text-sm md:flex md:max-w-[min(470px,42vw)] lg:max-w-[min(470px,30vw)] xl:max-w-[min(470px,36vw)] 2xl:w-[470px] 2xl:max-w-[470px] 2xl:basis-auto 2xl:flex-none site-input">
|
||||
<button
|
||||
type="button"
|
||||
class="site-header__search site-header__search--responsive hidden h-9 min-w-0 flex-1 basis-0 cursor-pointer items-center rounded-lg px-3 text-left text-sm transition-colors hover:opacity-90 md:flex md:max-w-[min(470px,42vw)] lg:max-w-[min(470px,30vw)] xl:max-w-[min(470px,36vw)] 2xl:w-[470px] 2xl:max-w-[470px] 2xl:basis-auto 2xl:flex-none site-input"
|
||||
aria-label="검색 열기"
|
||||
@click="openSearchModal"
|
||||
>
|
||||
<span class="site-header__search-icon mr-2 text-lg leading-none">⌕</span>
|
||||
<span class="site-header__search-text site-soft">Search</span>
|
||||
<span class="site-header__search-key ml-auto rounded-md px-2 text-xs site-soft site-input">/</span>
|
||||
</div>
|
||||
</button>
|
||||
<nav class="site-header__nav flex shrink-0 items-center gap-3 text-sm sm:gap-3.5">
|
||||
<NuxtLink class="site-header__buy site-accent-button shrink-0 rounded-lg px-3 py-1.5 text-xs font-semibold sm:px-4 sm:py-2 sm:text-sm" to="/pages/about">
|
||||
Subscribe
|
||||
@@ -184,4 +234,5 @@ onBeforeUnmount(() => {
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<SiteSearchModal v-model="searchOpen" />
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user