관리자 팝오버 표면 공통화

This commit is contained in:
2026-07-23 17:41:39 +09:00
parent 60512ef07d
commit 7dc1ecef4d
11 changed files with 78 additions and 25 deletions

View File

@@ -648,14 +648,18 @@ watch(() => props.member, () => {
<circle cx="12" cy="12.001" r="4.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
</svg>
</button>
<div v-if="actionMenuOpen" class="admin-member-form__action-popover absolute right-0 top-12 z-20 grid w-52 overflow-hidden rounded-xl border border-line bg-white py-2 text-sm text-[#3f4650] shadow-[0_16px_44px_rgba(15,23,42,0.16)]">
<AdminPopoverSurface
v-if="actionMenuOpen"
root-class="admin-member-form__action-popover absolute right-0 top-12 z-20 grid w-52 rounded-xl border-line py-2 text-sm text-[#3f4650]"
clipped
>
<button class="admin-member-form__action-item px-4 py-2.5 text-left hover:bg-[#f3f5f7]" type="button" @click="openPasswordModal">
비밀번호 변경
</button>
<button class="admin-member-form__action-item px-4 py-2.5 text-left text-[#d21a26] hover:bg-[#fff1f2]" type="button" @click="openDeleteModal">
멤버 삭제
</button>
</div>
</AdminPopoverSurface>
</div>
<button v-else class="admin-member-form__action-icon flex h-11 w-11 items-center justify-center rounded-md border border-line bg-white text-[#3f4650]" type="button" aria-label="멤버 작업" disabled>
<svg class="h-5 w-5" viewBox="0 0 24 24" aria-hidden="true">

View File

@@ -843,9 +843,9 @@ defineExpose({
</svg>
</button>
</div>
<div
<AdminPopoverSurface
v-if="isPageSectionSuggestionsOpen && hasPageSectionSuggestions"
class="admin-page-form__section-suggestions absolute left-0 right-0 top-[calc(100%+0.25rem)] z-20 max-h-48 overflow-y-auto rounded border border-[#e3e6e8] bg-white py-1 shadow-lg"
root-class="admin-page-form__section-suggestions absolute left-0 right-0 top-[calc(100%+0.25rem)] z-20 max-h-48 overflow-y-auto rounded border-[#e3e6e8] py-1"
>
<button
v-for="(option, index) in pageSectionSuggestionOptions"
@@ -859,13 +859,14 @@ defineExpose({
<span class="truncate">{{ option.name }}</span>
<span v-if="option.isSelected" class="text-xs text-[#8e9cac]">선택됨</span>
</button>
</div>
<p
</AdminPopoverSurface>
<AdminPopoverSurface
v-else-if="isPageSectionSuggestionsOpen && pageSectionInput.trim()"
class="admin-page-form__section-empty absolute left-0 right-0 top-[calc(100%+0.25rem)] z-20 rounded border border-[#e3e6e8] bg-white px-3 py-2 text-xs text-[#7c8b9a] shadow-lg"
as="p"
root-class="admin-page-form__section-empty absolute left-0 right-0 top-[calc(100%+0.25rem)] z-20 rounded border-[#e3e6e8] px-3 py-2 text-xs text-[#7c8b9a]"
>
일치하는 섹션이 없습니다.
</p>
</AdminPopoverSurface>
</div>
<span class="admin-page-form__help text-xs text-[#7c8b9a]">
섹션은 네비게이션의 페이지 섹션에서 추가합니다.

View File

@@ -0,0 +1,41 @@
<script setup>
const props = defineProps({
/** 렌더링할 HTML 태그 */
as: {
type: String,
default: 'div'
},
/** 팝오버 접근성 role */
role: {
type: String,
default: ''
},
/** 팝오버에 추가할 화면별 클래스 */
rootClass: {
type: String,
default: ''
},
/** 팝오버에 적용할 인라인 스타일 */
surfaceStyle: {
type: Object,
default: () => ({})
},
/** 기본 overflow 숨김 사용 여부 */
clipped: {
type: Boolean,
default: false
}
})
</script>
<template>
<component
:is="props.as"
class="admin-popover-surface border bg-white shadow-[0_18px_50px_rgba(15,23,42,0.14)]"
:class="[clipped ? 'overflow-hidden' : '', rootClass]"
:style="surfaceStyle"
:role="role || undefined"
>
<slot />
</component>
</template>

View File

@@ -2281,9 +2281,9 @@ defineExpose({
<p class="mt-1 text-xs text-[#8e9cac]">
{{ selectedTags.length }} / {{ postTagLimit }}개 선택됨
</p>
<div
<AdminPopoverSurface
v-if="hasTagSuggestions"
class="admin-post-form__tag-suggestions absolute left-0 right-0 top-full z-40 mt-1 max-h-64 overflow-y-auto rounded-lg border border-[#d7dce0] bg-white py-1 text-sm shadow-[0_18px_50px_rgba(15,23,42,0.14)]"
root-class="admin-post-form__tag-suggestions absolute left-0 right-0 top-full z-40 mt-1 max-h-64 overflow-y-auto rounded-lg border-[#d7dce0] py-1 text-sm"
role="listbox"
>
<button
@@ -2300,7 +2300,7 @@ defineExpose({
<span class="admin-post-form__tag-suggestion-name min-w-0 truncate font-semibold text-[#15171a]">{{ tag.name }}</span>
<span class="admin-post-form__tag-suggestion-meta shrink-0 text-xs text-muted">{{ getTagSuggestionMeta(tag) }}</span>
</button>
</div>
</AdminPopoverSurface>
</div>
</div>

View File

@@ -150,15 +150,16 @@ onBeforeUnmount(() => {
</svg>
</button>
<Teleport to="body">
<div
<AdminPopoverSurface
v-if="isOpen"
class="admin-row-more-menu__popover fixed z-[80] overflow-hidden rounded-xl border border-[#e2e5e9] bg-white py-2 text-sm text-[#3f4650] shadow-[0_18px_50px_rgba(15,23,42,0.16)]"
:style="popoverStyle"
root-class="admin-row-more-menu__popover fixed z-[80] rounded-xl border-[#e2e5e9] py-2 text-sm text-[#3f4650]"
:surface-style="popoverStyle"
clipped
role="menu"
data-admin-row-menu
>
<slot />
</div>
</AdminPopoverSurface>
</Teleport>
</div>
</template>

View File

@@ -190,6 +190,7 @@
| components/admin/AdminUnsavedChangesModal.vue | 관리자 편집 화면 공통 미저장 변경사항 이탈 확인 모달(상단 40px 위치) |
| components/admin/AdminConfirmModal.vue | 관리자 공통 확인 모달(제목·본문 슬롯·확인/취소 버튼, primary/danger 톤) |
| components/admin/AdminModalFrame.vue | 관리자 공통 일반 모달 프레임(오버레이, dialog 접근성 속성, 기본 패널 스타일, 바깥 클릭 닫기) |
| components/admin/AdminPopoverSurface.vue | 관리자 공통 팝오버 표면(border·shadow·radius 기반 스타일, 동적 태그·role·인라인 위치 스타일 전달) |
| components/admin/AdminToast.vue | 관리자 공통 우측 상단 토스트 표시(성공·오류·안내 톤, 화면별 상단 위치 조정) |
| components/admin/AdminSearchInput.vue | 관리자 공통 검색 입력(돋보기 아이콘, IME 검색 이벤트 전달, 외부 focus/select 노출) |
| components/admin/AdminSelectInput.vue | 관리자 공통 select 입력(문자·숫자 옵션 값, 기본 필터·정렬 옵션, chevron 아이콘, `v-model` 전달) |

View File

@@ -9,7 +9,6 @@
- `components/admin/AdminPostForm.vue`: 태그 입력, 대표 이미지 선택, 발행/초안 저장 상태, 우측 설정 패널 책임을 분리.
- `pages/admin/media/index.vue`: 미디어 목록, 폴더 트리, 상세 모달, 재생성 액션을 작은 단위로 분리.
- 관리자 UI 공통 컴포넌트 정리.
- 필터 팝오버의 radius·shadow·z-index·색상 규칙을 공통화.
- 멤버 고급 필터와 설정 화면 select 입력의 공통 컴포넌트 적용 범위 검토.
- 최근 브라우저 회귀 테스트 추가.
- 태그 추천 선택 후 다음 태그 후보가 포커스 해제 없이 유지되는지 확인.

View File

@@ -1,5 +1,10 @@
# 업데이트 이력
## v1.11.12
- 관리자 팝오버 표면 스타일을 공용 `AdminPopoverSurface` 컴포넌트로 분리.
- 글쓰기 태그 추천, 게시글 태그 필터, 페이지 섹션 추천, 멤버 작업 메뉴, 행 더보기 메뉴의 반복 border·shadow·radius 마크업 정리.
## v1.11.11
- 관리자 일반 모달 오버레이·패널을 공용 `AdminModalFrame` 컴포넌트로 분리.

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "sori.studio",
"version": "1.11.11",
"version": "1.11.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sori.studio",
"version": "1.11.11",
"version": "1.11.12",
"hasInstallScript": true,
"dependencies": {
"@nuxtjs/tailwindcss": "^6.14.0",

View File

@@ -1,6 +1,6 @@
{
"name": "sori.studio",
"version": "1.11.11",
"version": "1.11.12",
"private": true,
"type": "module",
"imports": {

View File

@@ -1074,10 +1074,10 @@ watch(postPageCount, (pageCount) => {
</svg>
</button>
</div>
<div
<AdminPopoverSurface
v-if="isTagFilterOpen && hasTagFilterSuggestions"
id="admin-posts-tag-filter-list"
class="admin-posts__tag-filter-list absolute left-0 right-0 top-[calc(100%+0.25rem)] z-30 max-h-64 overflow-y-auto rounded-lg border border-[#d7dce0] bg-white py-1 text-sm shadow-[0_18px_50px_rgba(15,23,42,0.14)]"
root-class="admin-posts__tag-filter-list absolute left-0 right-0 top-[calc(100%+0.25rem)] z-30 max-h-64 overflow-y-auto rounded-lg border-[#d7dce0] py-1 text-sm"
role="listbox"
>
<button
@@ -1094,13 +1094,14 @@ watch(postPageCount, (pageCount) => {
<span class="min-w-0 truncate">{{ option.name }}</span>
<span v-if="option.slug === tagFilter" class="shrink-0 text-xs text-[#8e9cac]">선택됨</span>
</button>
</div>
<p
</AdminPopoverSurface>
<AdminPopoverSurface
v-else-if="isTagFilterOpen && tagFilterSearch.trim()"
class="admin-posts__tag-filter-empty absolute left-0 right-0 top-[calc(100%+0.25rem)] z-30 rounded border border-[#d7dce0] bg-white px-3 py-2 text-xs text-muted shadow-[0_18px_50px_rgba(15,23,42,0.14)]"
as="p"
root-class="admin-posts__tag-filter-empty absolute left-0 right-0 top-[calc(100%+0.25rem)] z-30 rounded border-[#d7dce0] px-3 py-2 text-xs text-muted"
>
일치하는 태그가 없습니다.
</p>
</AdminPopoverSurface>
</div>
<AdminSelectInput
v-model="featuredFilter"