관리자 목록 선택 입력 공통화
This commit is contained in:
67
components/admin/AdminSelectInput.vue
Normal file
67
components/admin/AdminSelectInput.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
/** 선택 입력 값 */
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/** 선택 옵션 목록 */
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
/** 스크린 리더용 선택 라벨 */
|
||||
label: {
|
||||
type: String,
|
||||
default: '선택'
|
||||
},
|
||||
/** label 루트에 추가할 화면별 클래스 */
|
||||
rootClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/** select에 추가할 화면별 클래스 */
|
||||
selectClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/** select 너비 클래스 */
|
||||
widthClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
/**
|
||||
* 선택 값을 상위 컴포넌트에 전달한다.
|
||||
* @param {Event} event - 변경 이벤트
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateValue = (event) => {
|
||||
emit('update:modelValue', event.target?.value || '')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label class="admin-select-input" :class="rootClass">
|
||||
<span class="sr-only">{{ label }}</span>
|
||||
<span class="admin-select-input__wrap relative block">
|
||||
<select
|
||||
:value="props.modelValue"
|
||||
class="admin-select-input__field h-10 appearance-none rounded border border-line bg-white pl-3 pr-9 text-sm text-[#394047] outline-none transition-colors hover:border-[#c8ced3] focus:border-[#8e9cac]"
|
||||
:class="[widthClass, selectClass]"
|
||||
:aria-label="label"
|
||||
@change="updateValue"
|
||||
>
|
||||
<option v-for="option in options" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
<svg class="admin-select-input__chevron pointer-events-none absolute right-3 top-1/2 size-4 -translate-y-1/2 text-[#394047]" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
</span>
|
||||
</label>
|
||||
</template>
|
||||
@@ -189,6 +189,7 @@
|
||||
| components/admin/AdminConfirmModal.vue | 관리자 공통 확인 모달(제목·본문 슬롯·확인/취소 버튼, primary/danger 톤) |
|
||||
| components/admin/AdminToast.vue | 관리자 공통 우측 상단 토스트 표시(성공·오류·안내 톤, 화면별 상단 위치 조정) |
|
||||
| components/admin/AdminSearchInput.vue | 관리자 공통 검색 입력(돋보기 아이콘, IME 검색 이벤트 전달, 외부 focus/select 노출) |
|
||||
| components/admin/AdminSelectInput.vue | 관리자 공통 select 입력(기본 필터·정렬 옵션, chevron 아이콘, `v-model` 전달) |
|
||||
| components/admin/AdminPagination.vue | 관리자 공통 번호형 페이지네이션(이전·다음 화살표, 첫/끝/현재 주변 페이지, ellipsis 접기) |
|
||||
| components/admin/AdminAnalyticsTrendChart.vue | 관리자 통계 추이 막대 차트(축 라벨·증감 툴팁·기간 페이지 이동) |
|
||||
| lib/analytics-chart-ui.js | 통계 차트 라벨·페이지네이션·증감 계산 유틸 |
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
- `pages/admin/media/index.vue`: 미디어 목록, 폴더 트리, 상세 모달, 재생성 액션을 작은 단위로 분리.
|
||||
- 관리자 UI 공통 컴포넌트 정리.
|
||||
- 모달 패널, 필터 팝오버의 radius·shadow·z-index·색상 규칙을 공통화.
|
||||
- 게시글/페이지/미디어 목록의 필터·정렬 레이아웃을 같은 패턴으로 유지.
|
||||
- 게시글/페이지/미디어 목록의 고급 필터 레이아웃을 같은 패턴으로 유지.
|
||||
- 최근 브라우저 회귀 테스트 추가.
|
||||
- 태그 추천 선택 후 다음 태그 후보가 포커스 해제 없이 유지되는지 확인.
|
||||
- 관리자 미디어 대표 이미지/본문 이미지 탭 전환 중 역할 배지가 노출되지 않는지 확인.
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# 업데이트 이력
|
||||
|
||||
## v1.11.8
|
||||
|
||||
- 관리자 목록 select 입력을 공용 `AdminSelectInput` 컴포넌트로 분리.
|
||||
- 게시글·페이지 목록의 상태·추천·노출·정렬 필터 select 마크업과 chevron 중복 제거.
|
||||
|
||||
## v1.11.7
|
||||
|
||||
- 관리자 목록 검색 입력을 공용 `AdminSearchInput` 컴포넌트로 분리.
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.11.7",
|
||||
"version": "1.11.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sori.studio",
|
||||
"version": "1.11.7",
|
||||
"version": "1.11.8",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.11.7",
|
||||
"version": "1.11.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"imports": {
|
||||
|
||||
@@ -40,6 +40,27 @@ const analyticsMetricOptions = [
|
||||
{ label: '방문자', value: 'visitors' }
|
||||
]
|
||||
|
||||
/** 페이지 상태 필터 옵션 */
|
||||
const pageStatusFilterOptions = [
|
||||
{ value: 'all', label: '전체 상태' },
|
||||
{ value: 'published', label: '공개' },
|
||||
{ value: 'draft', label: '초안' },
|
||||
{ value: 'private', label: '비공개' }
|
||||
]
|
||||
|
||||
/** 페이지 홈 노출 필터 옵션 */
|
||||
const pageListedFilterOptions = [
|
||||
{ value: 'all', label: '전체 페이지' },
|
||||
{ value: 'listed', label: '홈 표시' },
|
||||
{ value: 'featured', label: '추천만' }
|
||||
]
|
||||
|
||||
/** 페이지 목록 정렬 옵션 */
|
||||
const pageSortOptions = [
|
||||
{ value: 'newest', label: '최신순' },
|
||||
{ value: 'oldest', label: '오래된순' }
|
||||
]
|
||||
|
||||
/**
|
||||
* 페이지별 통계 기본값을 생성한다.
|
||||
* @returns {Object} 페이지별 통계 기본값
|
||||
@@ -384,39 +405,27 @@ const deletePage = async (page) => {
|
||||
@compositionupdate="onSearchInput"
|
||||
@compositionend="onSearchCompositionEnd"
|
||||
/>
|
||||
<label class="admin-pages__filter">
|
||||
<span class="sr-only">상태 필터</span>
|
||||
<span class="admin-pages__filter-select-wrap relative block">
|
||||
<select v-model="statusFilter" class="admin-pages__filter-select h-10 appearance-none rounded border border-line bg-white pl-3 pr-9 text-sm text-[#394047] outline-none transition-colors hover:border-[#c8ced3] focus:border-[#8e9cac]">
|
||||
<option value="all">전체 상태</option>
|
||||
<option value="published">공개</option>
|
||||
<option value="draft">초안</option>
|
||||
<option value="private">비공개</option>
|
||||
</select>
|
||||
<svg class="pointer-events-none absolute right-3 top-1/2 size-4 -translate-y-1/2 text-[#394047]" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
||||
</span>
|
||||
</label>
|
||||
<label class="admin-pages__filter">
|
||||
<span class="sr-only">노출 필터</span>
|
||||
<span class="admin-pages__filter-select-wrap relative block">
|
||||
<select v-model="listedFilter" class="admin-pages__filter-select h-10 appearance-none rounded border border-line bg-white pl-3 pr-9 text-sm text-[#394047] outline-none transition-colors hover:border-[#c8ced3] focus:border-[#8e9cac]">
|
||||
<option value="all">전체 페이지</option>
|
||||
<option value="listed">홈 표시</option>
|
||||
<option value="featured">추천만</option>
|
||||
</select>
|
||||
<svg class="pointer-events-none absolute right-3 top-1/2 size-4 -translate-y-1/2 text-[#394047]" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
||||
</span>
|
||||
</label>
|
||||
<label class="admin-pages__filter">
|
||||
<span class="sr-only">정렬</span>
|
||||
<span class="admin-pages__filter-select-wrap relative block">
|
||||
<select v-model="sortOrder" class="admin-pages__filter-select h-10 appearance-none rounded border border-line bg-white pl-3 pr-9 text-sm text-[#394047] outline-none transition-colors hover:border-[#c8ced3] focus:border-[#8e9cac]">
|
||||
<option value="newest">최신순</option>
|
||||
<option value="oldest">오래된순</option>
|
||||
</select>
|
||||
<svg class="pointer-events-none absolute right-3 top-1/2 size-4 -translate-y-1/2 text-[#394047]" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
||||
</span>
|
||||
</label>
|
||||
<AdminSelectInput
|
||||
v-model="statusFilter"
|
||||
:options="pageStatusFilterOptions"
|
||||
label="상태 필터"
|
||||
root-class="admin-pages__filter"
|
||||
select-class="admin-pages__filter-select"
|
||||
/>
|
||||
<AdminSelectInput
|
||||
v-model="listedFilter"
|
||||
:options="pageListedFilterOptions"
|
||||
label="노출 필터"
|
||||
root-class="admin-pages__filter"
|
||||
select-class="admin-pages__filter-select"
|
||||
/>
|
||||
<AdminSelectInput
|
||||
v-model="sortOrder"
|
||||
:options="pageSortOptions"
|
||||
label="정렬"
|
||||
root-class="admin-pages__filter"
|
||||
select-class="admin-pages__filter-select"
|
||||
/>
|
||||
</div>
|
||||
<NuxtLink class="admin-pages__new shrink-0 rounded bg-[#15171a] px-4 py-2 text-sm font-semibold text-white" to="/admin/pages/new">
|
||||
새 페이지
|
||||
|
||||
@@ -94,6 +94,28 @@ const analyticsMetricOptions = [
|
||||
{ label: '방문자', value: 'visitors' }
|
||||
]
|
||||
|
||||
/** 게시글 상태 필터 옵션 */
|
||||
const postStatusFilterOptions = [
|
||||
{ value: 'all', label: '전체 상태' },
|
||||
{ value: 'published', label: '공개' },
|
||||
{ value: 'draft', label: '초안' },
|
||||
{ value: 'scheduled', label: '예약' },
|
||||
{ value: 'members', label: '멤버십' },
|
||||
{ value: 'private', label: '비공개' }
|
||||
]
|
||||
|
||||
/** 게시글 추천 필터 옵션 */
|
||||
const postFeaturedFilterOptions = [
|
||||
{ value: 'all', label: '전체 글' },
|
||||
{ value: 'featured', label: '추천만' }
|
||||
]
|
||||
|
||||
/** 게시글 목록 정렬 옵션 */
|
||||
const postSortOptions = [
|
||||
{ value: 'newest', label: '최신순' },
|
||||
{ value: 'oldest', label: '오래된순' }
|
||||
]
|
||||
|
||||
/**
|
||||
* 게시물별 통계 기본값을 생성한다.
|
||||
* @returns {Object} 게시물별 통계 기본값
|
||||
@@ -1000,20 +1022,13 @@ watch(postPageCount, (pageCount) => {
|
||||
@compositionupdate="onSearchInput"
|
||||
@compositionend="onSearchCompositionEnd"
|
||||
/>
|
||||
<label class="admin-posts__filter">
|
||||
<span class="sr-only">상태 필터</span>
|
||||
<span class="admin-posts__filter-select-wrap relative block">
|
||||
<select v-model="statusFilter" class="admin-posts__filter-select h-10 appearance-none rounded border border-line bg-white pl-3 pr-9 text-sm text-[#394047] outline-none transition-colors hover:border-[#c8ced3] focus:border-[#8e9cac]">
|
||||
<option value="all">전체 상태</option>
|
||||
<option value="published">공개</option>
|
||||
<option value="draft">초안</option>
|
||||
<option value="scheduled">예약</option>
|
||||
<option value="members">멤버십</option>
|
||||
<option value="private">비공개</option>
|
||||
</select>
|
||||
<svg class="pointer-events-none absolute right-3 top-1/2 size-4 -translate-y-1/2 text-[#394047]" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
||||
</span>
|
||||
</label>
|
||||
<AdminSelectInput
|
||||
v-model="statusFilter"
|
||||
:options="postStatusFilterOptions"
|
||||
label="상태 필터"
|
||||
root-class="admin-posts__filter"
|
||||
select-class="admin-posts__filter-select"
|
||||
/>
|
||||
<div
|
||||
ref="tagFilterRef"
|
||||
class="admin-posts__tag-filter relative w-52"
|
||||
@@ -1087,26 +1102,20 @@ watch(postPageCount, (pageCount) => {
|
||||
일치하는 태그가 없습니다.
|
||||
</p>
|
||||
</div>
|
||||
<label class="admin-posts__filter">
|
||||
<span class="sr-only">추천 필터</span>
|
||||
<span class="admin-posts__filter-select-wrap relative block">
|
||||
<select v-model="featuredFilter" class="admin-posts__filter-select h-10 appearance-none rounded border border-line bg-white pl-3 pr-9 text-sm text-[#394047] outline-none transition-colors hover:border-[#c8ced3] focus:border-[#8e9cac]">
|
||||
<option value="all">전체 글</option>
|
||||
<option value="featured">추천만</option>
|
||||
</select>
|
||||
<svg class="pointer-events-none absolute right-3 top-1/2 size-4 -translate-y-1/2 text-[#394047]" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
||||
</span>
|
||||
</label>
|
||||
<label class="admin-posts__filter">
|
||||
<span class="sr-only">정렬</span>
|
||||
<span class="admin-posts__filter-select-wrap relative block">
|
||||
<select v-model="sortOrder" class="admin-posts__filter-select h-10 appearance-none rounded border border-line bg-white pl-3 pr-9 text-sm text-[#394047] outline-none transition-colors hover:border-[#c8ced3] focus:border-[#8e9cac]">
|
||||
<option value="newest">최신순</option>
|
||||
<option value="oldest">오래된순</option>
|
||||
</select>
|
||||
<svg class="pointer-events-none absolute right-3 top-1/2 size-4 -translate-y-1/2 text-[#394047]" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
||||
</span>
|
||||
</label>
|
||||
<AdminSelectInput
|
||||
v-model="featuredFilter"
|
||||
:options="postFeaturedFilterOptions"
|
||||
label="추천 필터"
|
||||
root-class="admin-posts__filter"
|
||||
select-class="admin-posts__filter-select"
|
||||
/>
|
||||
<AdminSelectInput
|
||||
v-model="sortOrder"
|
||||
:options="postSortOptions"
|
||||
label="정렬"
|
||||
root-class="admin-posts__filter"
|
||||
select-class="admin-posts__filter-select"
|
||||
/>
|
||||
</div>
|
||||
<NuxtLink class="admin-posts__new shrink-0 rounded bg-[#15171a] px-4 py-2 text-sm font-semibold text-white" to="/admin/posts/new">
|
||||
새 글
|
||||
|
||||
Reference in New Issue
Block a user