관리자 검색창 한글 IME 조합 검색 및 모바일 가로 트랙 세로 스크롤 문제 수정
- IME 조합 중에도 검색되는 공통 컴포저블 useImeSearchInput 추가 - 글/멤버/미디어/설정/태그 필터/미디어 모달 검색 IME 조합 대응 - 홈 추천글·페이지 섹션 가로 트랙 touch-action pan-x 제거로 모바일 세로 스크롤 복구 - v1.10.29 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -98,6 +98,11 @@ const isUploading = ref(false)
|
|||||||
const mediaPickerTarget = ref('image')
|
const mediaPickerTarget = ref('image')
|
||||||
const activeMediaPickerTab = ref('upload')
|
const activeMediaPickerTab = ref('upload')
|
||||||
const mediaSearchQuery = ref('')
|
const mediaSearchQuery = ref('')
|
||||||
|
const {
|
||||||
|
displayValue: mediaSearchInput,
|
||||||
|
onInput: onMediaSearchInput,
|
||||||
|
onCompositionEnd: onMediaSearchCompositionEnd
|
||||||
|
} = useImeSearchInput(mediaSearchQuery)
|
||||||
const selectedMediaUrls = ref([])
|
const selectedMediaUrls = ref([])
|
||||||
const lastSelectionState = ref({
|
const lastSelectionState = ref({
|
||||||
start: 0,
|
start: 0,
|
||||||
@@ -3792,11 +3797,14 @@ const handleKeydown = (event) => {
|
|||||||
<label v-if="activeMediaPickerTab === 'library'" class="admin-markdown-editor__media-search mb-px flex w-full max-w-xs shrink-0 items-center py-2">
|
<label v-if="activeMediaPickerTab === 'library'" class="admin-markdown-editor__media-search mb-px flex w-full max-w-xs shrink-0 items-center py-2">
|
||||||
<span class="sr-only">파일명 검색</span>
|
<span class="sr-only">파일명 검색</span>
|
||||||
<input
|
<input
|
||||||
v-model="mediaSearchQuery"
|
:value="mediaSearchInput"
|
||||||
class="admin-markdown-editor__media-search-input w-full rounded border border-[#d7dde2] bg-white px-3 py-1.5 text-sm text-[#394047] outline-none transition-colors placeholder:text-[#8e9cac] focus:border-[#15171a] focus:ring-1 focus:ring-[#15171a]/20"
|
class="admin-markdown-editor__media-search-input w-full rounded border border-[#d7dde2] bg-white px-3 py-1.5 text-sm text-[#394047] outline-none transition-colors placeholder:text-[#8e9cac] focus:border-[#15171a] focus:ring-1 focus:ring-[#15171a]/20"
|
||||||
type="search"
|
type="search"
|
||||||
placeholder="파일명 검색"
|
placeholder="파일명 검색"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
@input="onMediaSearchInput"
|
||||||
|
@compositionupdate="onMediaSearchInput"
|
||||||
|
@compositionend="onMediaSearchCompositionEnd"
|
||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -812,6 +812,7 @@ defineExpose({
|
|||||||
:disabled="pageSectionOptions.length === 0"
|
:disabled="pageSectionOptions.length === 0"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
@input="openPageSectionSuggestions"
|
@input="openPageSectionSuggestions"
|
||||||
|
@compositionupdate="pageSectionInput = $event.target.value; openPageSectionSuggestions()"
|
||||||
@focus="openPageSectionSuggestions"
|
@focus="openPageSectionSuggestions"
|
||||||
@blur="handlePageSectionBlur"
|
@blur="handlePageSectionBlur"
|
||||||
@keydown="handlePageSectionKeydown"
|
@keydown="handlePageSectionKeydown"
|
||||||
|
|||||||
@@ -2041,6 +2041,7 @@ defineExpose({
|
|||||||
aria-autocomplete="list"
|
aria-autocomplete="list"
|
||||||
@focus="openTagSuggestions"
|
@focus="openTagSuggestions"
|
||||||
@input="openTagSuggestions"
|
@input="openTagSuggestions"
|
||||||
|
@compositionupdate="tagInput = $event.target.value; openTagSuggestions()"
|
||||||
@blur="handleTagBlur"
|
@blur="handleTagBlur"
|
||||||
@keydown="handleTagKeydown"
|
@keydown="handleTagKeydown"
|
||||||
@compositionstart="isTagInputComposing = true"
|
@compositionstart="isTagInputComposing = true"
|
||||||
|
|||||||
84
composables/useImeSearchInput.js
Normal file
84
composables/useImeSearchInput.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
| composables/formatPostDate.js | 공개 게시일 `YYYY.MM.DD`, 관리자·수정일 보조 `formatPostDateTime`, `wasPostUpdatedAfterPublish` |
|
| composables/formatPostDate.js | 공개 게시일 `YYYY.MM.DD`, 관리자·수정일 보조 `formatPostDateTime`, `wasPostUpdatedAfterPublish` |
|
||||||
| composables/createPostSummary.js | 게시물 요약·본문에서 목록·SEO용 짧은 설명 텍스트 생성, 본문 대체 요약의 표시용 마크다운·HTML 토큰 제거 |
|
| composables/createPostSummary.js | 게시물 요약·본문에서 목록·SEO용 짧은 설명 텍스트 생성, 본문 대체 요약의 표시용 마크다운·HTML 토큰 제거 |
|
||||||
| composables/useAdminToast.js | 관리자 우측 상단 토스트(성공·오류·안내; 모달보다 위 z-index) |
|
| composables/useAdminToast.js | 관리자 우측 상단 토스트(성공·오류·안내; 모달보다 위 z-index) |
|
||||||
|
| composables/useImeSearchInput.js | 한글 등 IME 조합 중(`compositionupdate`)에도 검색어가 즉시 반영되는 검색 입력 처리기(표시값·`onInput`·`onCompositionEnd`, NFC 정규화·선택적 디바운스) |
|
||||||
|
|
||||||
## 공유 라이브러리(서버·클라이언트 공통)
|
## 공유 라이브러리(서버·클라이언트 공통)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
# 업데이트 이력
|
# 업데이트 이력
|
||||||
|
|
||||||
|
## v1.10.29
|
||||||
|
|
||||||
|
- 한글 IME 조합 중(`compositionupdate`)에도 검색이 즉시 반영되는 공통 컴포저블 `useImeSearchInput` 추가(NFC 정규화·선택적 디바운스).
|
||||||
|
- 관리자 글 목록·멤버 목록·미디어 목록·설정 검색·태그 필터·글 목록 태그 필터 콤보박스·글쓰기 미디어 모달 검색을 모두 IME 조합 중 검색되도록 수정.
|
||||||
|
- 관리자 글 목록 검색 200ms, 미디어 검색 250ms 디바운스 적용.
|
||||||
|
- 글쓰기 태그 입력·페이지 섹션 콤보박스가 조합 중에도 추천 목록을 갱신하도록 `compositionupdate` 보강(Enter 확정 로직 유지).
|
||||||
|
- 홈 추천글·페이지 섹션 가로 트랙의 `touch-action: pan-x` 제거로 모바일 세로 스크롤이 막히던 문제 수정.
|
||||||
|
- 홈 가로 캐러셀 커스텀 드래그를 마우스 전용으로 제한하고, 터치는 네이티브 가로 스크롤·스냅에 맡기도록 수정.
|
||||||
|
|
||||||
## v1.10.28
|
## v1.10.28
|
||||||
|
|
||||||
- 공개 통합 검색용 `posts.search_text`, `pages.search_text` 컬럼과 `pg_trgm` GIN 인덱스 추가.
|
- 공개 통합 검색용 `posts.search_text`, `pages.search_text` 컬럼과 `pg_trgm` GIN 인덱스 추가.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "1.10.28",
|
"version": "1.10.29",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"imports": {
|
"imports": {
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ const isThumbnailFolderPath = (folder) => folder === MEDIA_THUMBNAIL_ROOT || Str
|
|||||||
|
|
||||||
const activeTab = ref('library')
|
const activeTab = ref('library')
|
||||||
const searchText = ref('')
|
const searchText = ref('')
|
||||||
|
const {
|
||||||
|
displayValue: searchTextInput,
|
||||||
|
onInput: onSearchTextInput,
|
||||||
|
onCompositionEnd: onSearchTextCompositionEnd
|
||||||
|
} = useImeSearchInput(searchText, { debounce: 250 })
|
||||||
const activeFolder = ref('')
|
const activeFolder = ref('')
|
||||||
const activeMediaKind = ref('all')
|
const activeMediaKind = ref('all')
|
||||||
const showUnusedOnly = ref(false)
|
const showUnusedOnly = ref(false)
|
||||||
@@ -1387,10 +1392,13 @@ watch(() => mediaPagination.value.page, (page) => {
|
|||||||
<circle cx="11" cy="11" r="8" />
|
<circle cx="11" cy="11" r="8" />
|
||||||
</svg>
|
</svg>
|
||||||
<input
|
<input
|
||||||
v-model="searchText"
|
:value="searchTextInput"
|
||||||
class="admin-media__search-input h-10 w-full rounded border border-line bg-white pl-9 pr-3 text-sm text-[#394047] outline-none transition-colors placeholder:text-[#8e9cac] hover:border-[#c8ced3] focus:border-[#8e9cac] md:w-72"
|
class="admin-media__search-input h-10 w-full rounded border border-line bg-white pl-9 pr-3 text-sm text-[#394047] outline-none transition-colors placeholder:text-[#8e9cac] hover:border-[#c8ced3] focus:border-[#8e9cac] md:w-72"
|
||||||
type="search"
|
type="search"
|
||||||
placeholder="미디어 검색"
|
placeholder="미디어 검색"
|
||||||
|
@input="onSearchTextInput"
|
||||||
|
@compositionupdate="onSearchTextInput"
|
||||||
|
@compositionend="onSearchTextCompositionEnd"
|
||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ definePageMeta({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const memberSearchQuery = ref('')
|
const memberSearchQuery = ref('')
|
||||||
|
const {
|
||||||
|
displayValue: memberSearchInput,
|
||||||
|
onInput: onMemberSearchInput,
|
||||||
|
onCompositionEnd: onMemberSearchCompositionEnd
|
||||||
|
} = useImeSearchInput(memberSearchQuery)
|
||||||
const isFilterOpen = ref(false)
|
const isFilterOpen = ref(false)
|
||||||
const filterDrafts = ref([
|
const filterDrafts = ref([
|
||||||
{
|
{
|
||||||
@@ -383,10 +388,13 @@ const formatRelativeTime = (value) => {
|
|||||||
<circle cx="11" cy="11" r="8" />
|
<circle cx="11" cy="11" r="8" />
|
||||||
</svg>
|
</svg>
|
||||||
<input
|
<input
|
||||||
v-model="memberSearchQuery"
|
:value="memberSearchInput"
|
||||||
class="admin-members__search-input h-10 w-full rounded border border-line bg-white pl-9 pr-3 text-sm text-[#394047] outline-none transition-colors placeholder:text-[#8e9cac] hover:border-[#c8ced3] focus:border-[#8e9cac]"
|
class="admin-members__search-input h-10 w-full rounded border border-line bg-white pl-9 pr-3 text-sm text-[#394047] outline-none transition-colors placeholder:text-[#8e9cac] hover:border-[#c8ced3] focus:border-[#8e9cac]"
|
||||||
type="search"
|
type="search"
|
||||||
placeholder="멤버 검색"
|
placeholder="멤버 검색"
|
||||||
|
@input="onMemberSearchInput"
|
||||||
|
@compositionupdate="onMemberSearchInput"
|
||||||
|
@compositionend="onMemberSearchCompositionEnd"
|
||||||
aria-label="멤버 검색"
|
aria-label="멤버 검색"
|
||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -29,6 +29,18 @@ const analyticsModalError = ref('')
|
|||||||
const postMenuTriggerRefs = new Map()
|
const postMenuTriggerRefs = new Map()
|
||||||
const tagFilterRef = ref(null)
|
const tagFilterRef = ref(null)
|
||||||
|
|
||||||
|
const {
|
||||||
|
displayValue: searchInput,
|
||||||
|
onInput: onSearchInput,
|
||||||
|
onCompositionEnd: onSearchCompositionEnd
|
||||||
|
} = useImeSearchInput(searchQuery, { debounce: 200 })
|
||||||
|
|
||||||
|
const {
|
||||||
|
displayValue: tagFilterSearchInput,
|
||||||
|
onInput: onTagFilterSearchInput,
|
||||||
|
onCompositionEnd: onTagFilterSearchCompositionEnd
|
||||||
|
} = useImeSearchInput(tagFilterSearch)
|
||||||
|
|
||||||
const postsQuery = computed(() => ({
|
const postsQuery = computed(() => ({
|
||||||
paged: 'true',
|
paged: 'true',
|
||||||
page: postPage.value,
|
page: postPage.value,
|
||||||
@@ -921,10 +933,13 @@ watch(postPageCount, (pageCount) => {
|
|||||||
<circle cx="11" cy="11" r="8" />
|
<circle cx="11" cy="11" r="8" />
|
||||||
</svg>
|
</svg>
|
||||||
<input
|
<input
|
||||||
v-model="searchQuery"
|
:value="searchInput"
|
||||||
class="admin-posts__search-input h-10 w-52 rounded border border-line bg-white pl-9 pr-3 text-sm text-[#394047] outline-none transition-colors placeholder:text-[#8e9cac] hover:border-[#c8ced3] focus:border-[#8e9cac]"
|
class="admin-posts__search-input h-10 w-52 rounded border border-line bg-white pl-9 pr-3 text-sm text-[#394047] outline-none transition-colors placeholder:text-[#8e9cac] hover:border-[#c8ced3] focus:border-[#8e9cac]"
|
||||||
type="search"
|
type="search"
|
||||||
placeholder="글 검색"
|
placeholder="글 검색"
|
||||||
|
@input="onSearchInput"
|
||||||
|
@compositionupdate="onSearchInput"
|
||||||
|
@compositionend="onSearchCompositionEnd"
|
||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
<label class="admin-posts__filter">
|
<label class="admin-posts__filter">
|
||||||
@@ -958,7 +973,7 @@ watch(postPageCount, (pageCount) => {
|
|||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
<input
|
<input
|
||||||
v-model="tagFilterSearch"
|
:value="tagFilterSearchInput"
|
||||||
class="admin-posts__tag-filter-input min-w-0 flex-1 bg-transparent px-1 text-sm text-[#394047] outline-none placeholder:text-[#394047]"
|
class="admin-posts__tag-filter-input min-w-0 flex-1 bg-transparent px-1 text-sm text-[#394047] outline-none placeholder:text-[#394047]"
|
||||||
type="text"
|
type="text"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
@@ -966,7 +981,9 @@ watch(postPageCount, (pageCount) => {
|
|||||||
aria-controls="admin-posts-tag-filter-list"
|
aria-controls="admin-posts-tag-filter-list"
|
||||||
:placeholder="tagFilter === 'all' ? '전체 태그' : '태그 검색'"
|
:placeholder="tagFilter === 'all' ? '전체 태그' : '태그 검색'"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
@input="openTagFilter"
|
@input="onTagFilterSearchInput($event); openTagFilter()"
|
||||||
|
@compositionupdate="onTagFilterSearchInput"
|
||||||
|
@compositionend="onTagFilterSearchCompositionEnd"
|
||||||
@focus="openTagFilter"
|
@focus="openTagFilter"
|
||||||
@keydown="handleTagFilterKeydown"
|
@keydown="handleTagFilterKeydown"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -87,6 +87,11 @@ const homeCoverDarkInputRef = ref(null)
|
|||||||
const postImportInputRef = ref(null)
|
const postImportInputRef = ref(null)
|
||||||
const mainScrollRef = ref(null)
|
const mainScrollRef = ref(null)
|
||||||
const navSearchQuery = ref('')
|
const navSearchQuery = ref('')
|
||||||
|
const {
|
||||||
|
displayValue: navSearchInput,
|
||||||
|
onInput: onNavSearchInput,
|
||||||
|
onCompositionEnd: onNavSearchCompositionEnd
|
||||||
|
} = useImeSearchInput(navSearchQuery)
|
||||||
const activeSectionId = ref('admin-settings-section-title')
|
const activeSectionId = ref('admin-settings-section-title')
|
||||||
const scrollSpySuspended = ref(false)
|
const scrollSpySuspended = ref(false)
|
||||||
/** 블로그 제목·설명 카드 편집 모드 여부 */
|
/** 블로그 제목·설명 카드 편집 모드 여부 */
|
||||||
@@ -1991,11 +1996,14 @@ onBeforeUnmount(() => {
|
|||||||
<path d="M16 16l4.5 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
<path d="M16 16l4.5 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||||
</svg>
|
</svg>
|
||||||
<input
|
<input
|
||||||
v-model="navSearchQuery"
|
:value="navSearchInput"
|
||||||
class="admin-settings-screen__search-input w-full rounded-md border border-[#dce0e5] bg-white py-2 pr-3 pl-9 text-sm text-[#15171a] placeholder:text-[#9aa3ad] outline-none focus:border-[#15171a] focus:ring-1 focus:ring-[#15171a]"
|
class="admin-settings-screen__search-input w-full rounded-md border border-[#dce0e5] bg-white py-2 pr-3 pl-9 text-sm text-[#15171a] placeholder:text-[#9aa3ad] outline-none focus:border-[#15171a] focus:ring-1 focus:ring-[#15171a]"
|
||||||
type="search"
|
type="search"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
placeholder="설정 검색"
|
placeholder="설정 검색"
|
||||||
|
@input="onNavSearchInput"
|
||||||
|
@compositionupdate="onNavSearchInput"
|
||||||
|
@compositionend="onNavSearchCompositionEnd"
|
||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ const TAG_TOAST_STORAGE_KEY = 'SORI_ADMIN_TAG_TOAST'
|
|||||||
const { openMenuId, closeMenu } = useAdminRowMenu()
|
const { openMenuId, closeMenu } = useAdminRowMenu()
|
||||||
let toastTimer = null
|
let toastTimer = null
|
||||||
const generalTagQuery = ref('')
|
const generalTagQuery = ref('')
|
||||||
|
const {
|
||||||
|
displayValue: generalTagQueryInput,
|
||||||
|
onInput: onGeneralTagQueryInput,
|
||||||
|
onCompositionEnd: onGeneralTagQueryCompositionEnd
|
||||||
|
} = useImeSearchInput(generalTagQuery)
|
||||||
const generalTagSortMode = ref('recent')
|
const generalTagSortMode = ref('recent')
|
||||||
|
|
||||||
const { data: tags, refresh } = await useFetch('/admin/api/tags', {
|
const { data: tags, refresh } = await useFetch('/admin/api/tags', {
|
||||||
@@ -474,10 +479,13 @@ onBeforeUnmount(() => {
|
|||||||
<div class="space-y-3 bg-white p-4">
|
<div class="space-y-3 bg-white p-4">
|
||||||
<div class="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
|
<div class="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
|
||||||
<input
|
<input
|
||||||
v-model="generalTagQuery"
|
:value="generalTagQueryInput"
|
||||||
type="text"
|
type="text"
|
||||||
class="h-10 min-w-0 flex-1 rounded border border-line px-3 text-sm outline-none focus:border-[#8e9cac]"
|
class="h-10 min-w-0 flex-1 rounded border border-line px-3 text-sm outline-none focus:border-[#8e9cac]"
|
||||||
placeholder="일반 태그 이름 또는 슬러그 필터"
|
placeholder="일반 태그 이름 또는 슬러그 필터"
|
||||||
|
@input="onGeneralTagQueryInput"
|
||||||
|
@compositionupdate="onGeneralTagQueryInput"
|
||||||
|
@compositionend="onGeneralTagQueryCompositionEnd"
|
||||||
@keydown.enter.prevent="searchGeneralTags"
|
@keydown.enter.prevent="searchGeneralTags"
|
||||||
>
|
>
|
||||||
<div class="inline-flex shrink-0 rounded border border-line bg-[#f7f7f5] p-1">
|
<div class="inline-flex shrink-0 rounded border border-line bg-[#f7f7f5] p-1">
|
||||||
|
|||||||
@@ -592,7 +592,11 @@ const bindCarouselDragDocumentEvents = () => {
|
|||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const startCarouselDrag = (event, key) => {
|
const startCarouselDrag = (event, key) => {
|
||||||
if (event.pointerType === 'mouse' && event.button !== 0) {
|
// 터치/펜은 브라우저 네이티브 가로 스크롤에 맡겨 세로(페이지) 스크롤이 막히지 않게 한다.
|
||||||
|
if (event.pointerType !== 'mouse') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.button !== 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -767,7 +771,7 @@ watch([latestPosts, hasHomeInfeedAd], () => {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
ref="featuredTrackRef"
|
ref="featuredTrackRef"
|
||||||
class="featured-posts-track mt-4 flex cursor-grab select-none snap-x snap-mandatory gap-6 overflow-x-auto overscroll-x-contain scroll-smooth pb-1 touch-pan-x active:cursor-grabbing [-webkit-overflow-scrolling:touch] [--slides:1.4] sm:[--slides:1.6] lg:[--slides:2.6] [-ms-overflow-style:none] [scrollbar-width:none] [&_*]:select-none [&_img]:pointer-events-none [&_img]:select-none [&::-webkit-scrollbar]:hidden"
|
class="featured-posts-track mt-4 flex cursor-grab select-none snap-x snap-mandatory gap-6 overflow-x-auto overscroll-x-contain scroll-smooth pb-1 active:cursor-grabbing [-webkit-overflow-scrolling:touch] [--slides:1.4] sm:[--slides:1.6] lg:[--slides:2.6] [-ms-overflow-style:none] [scrollbar-width:none] [&_*]:select-none [&_img]:pointer-events-none [&_img]:select-none [&::-webkit-scrollbar]:hidden"
|
||||||
@pointerdown="startCarouselDrag($event, 'featured')"
|
@pointerdown="startCarouselDrag($event, 'featured')"
|
||||||
@click.capture="preventCarouselClickAfterDrag"
|
@click.capture="preventCarouselClickAfterDrag"
|
||||||
@dragstart="preventNativeCarouselDrag"
|
@dragstart="preventNativeCarouselDrag"
|
||||||
@@ -841,7 +845,7 @@ watch([latestPosts, hasHomeInfeedAd], () => {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
:ref="(el) => setPageShelfTrackRef(shelf.id, el)"
|
:ref="(el) => setPageShelfTrackRef(shelf.id, el)"
|
||||||
class="home-pages-section__rail -mt-2 flex w-full max-w-full cursor-grab select-none snap-x snap-mandatory gap-4 overflow-x-auto overscroll-x-contain scroll-smooth pb-3 pt-6 touch-pan-x active:cursor-grabbing [-webkit-overflow-scrolling:touch] [scrollbar-width:none] [&_*]:select-none [&_img]:pointer-events-none [&_img]:select-none [&::-webkit-scrollbar]:hidden"
|
class="home-pages-section__rail -mt-2 flex w-full max-w-full cursor-grab select-none snap-x snap-mandatory gap-4 overflow-x-auto overscroll-x-contain scroll-smooth pb-3 pt-6 active:cursor-grabbing [-webkit-overflow-scrolling:touch] [scrollbar-width:none] [&_*]:select-none [&_img]:pointer-events-none [&_img]:select-none [&::-webkit-scrollbar]:hidden"
|
||||||
@scroll.passive="updatePageShelfScrollEdges(shelf.id, $event.currentTarget)"
|
@scroll.passive="updatePageShelfScrollEdges(shelf.id, $event.currentTarget)"
|
||||||
@pointerdown="startCarouselDrag($event, shelf.id)"
|
@pointerdown="startCarouselDrag($event, shelf.id)"
|
||||||
@click.capture="preventCarouselClickAfterDrag"
|
@click.capture="preventCarouselClickAfterDrag"
|
||||||
|
|||||||
Reference in New Issue
Block a user