Files
sori.studio/composables/useImeSearchInput.js
zenn b669d34e83 관리자 검색창 한글 IME 조합 검색 및 모바일 가로 트랙 세로 스크롤 문제 수정
- IME 조합 중에도 검색되는 공통 컴포저블 useImeSearchInput 추가
- 글/멤버/미디어/설정/태그 필터/미디어 모달 검색 IME 조합 대응
- 홈 추천글·페이지 섹션 가로 트랙 touch-action pan-x 제거로 모바일 세로 스크롤 복구
- v1.10.29

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-03 17:03:31 +09:00

85 lines
2.7 KiB
JavaScript

import { ref, watch } from 'vue'
/**
* 검색어를 NFC로 정규화한다.
* @param {string} value - 원본 입력값
* @returns {string} 정규화된 문자열
*/
const normalizeSearchValue = (value) => String(value ?? '').normalize('NFC')
/**
* 한글 등 IME 조합 중에도 검색어가 즉시 반영되는 검색 입력 처리기를 만든다.
*
* Vue의 `v-model`은 IME 조합 중에는 값을 갱신하지 않아, 조합이 끝나야(스페이스·다음 글자)
* 검색이 시작되는 문제가 있다. 이 컴포저블은 `compositionupdate`까지 반영해 조합 도중에도
* 검색이 되게 하고, 선택적으로 디바운스를 적용한다.
*
* @param {import('vue').Ref<string>} targetRef - 검색/필터에 사용할 대상 ref(정규화된 값이 기록됨)
* @param {Object} [options] - 옵션
* @param {number} [options.debounce=0] - 디바운스 지연(ms). 0이면 즉시 반영
* @returns {{ displayValue: import('vue').Ref<string>, onInput: (event: Event) => void, onCompositionEnd: (event: Event) => void }}
*/
export const useImeSearchInput = (targetRef, options = {}) => {
const debounce = Number(options.debounce) || 0
const displayValue = ref(String(targetRef.value ?? ''))
/** @type {ReturnType<typeof setTimeout> | null} */
let debounceTimer = null
/**
* 대상 ref에 정규화된 값을 반영한다(디바운스 적용).
* @param {string} value - 현재 입력값
* @returns {void}
*/
const commit = (value) => {
if (debounce > 0) {
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
targetRef.value = normalizeSearchValue(value)
}, debounce)
return
}
targetRef.value = normalizeSearchValue(value)
}
/**
* 입력 이벤트에서 표시값과 검색어를 갱신한다(IME 조합 중 포함).
* @param {Event} event - `input`/`compositionupdate` 이벤트
* @returns {void}
*/
const onInput = (event) => {
const target = event.target
if (!(target instanceof HTMLInputElement)) {
return
}
displayValue.value = target.value
commit(target.value)
}
/**
* IME 조합 종료 시 최종 검색어를 즉시 반영한다.
* @param {Event} event - `compositionend` 이벤트
* @returns {void}
*/
const onCompositionEnd = (event) => {
const target = event.target
if (target instanceof HTMLInputElement) {
displayValue.value = target.value
}
clearTimeout(debounceTimer)
targetRef.value = normalizeSearchValue(displayValue.value)
}
watch(targetRef, (value) => {
if (normalizeSearchValue(displayValue.value) !== value) {
displayValue.value = String(value ?? '')
}
})
return {
displayValue,
onInput,
onCompositionEnd
}
}