관리자 모달 프레임 공통화

This commit is contained in:
2026-07-23 17:27:10 +09:00
parent 57c4363db2
commit 60512ef07d
8 changed files with 422 additions and 387 deletions

View File

@@ -0,0 +1,44 @@
<script setup>
const props = defineProps({
/** 모달 제목 요소 ID */
labelledBy: {
type: String,
required: true
},
/** 오버레이에 추가할 화면별 클래스 */
rootClass: {
type: String,
default: ''
},
/** 패널에 추가할 화면별 클래스 */
panelClass: {
type: String,
default: ''
},
/** 오버레이 z-index 클래스 */
zClass: {
type: String,
default: 'z-[60]'
}
})
const emit = defineEmits(['close'])
</script>
<template>
<div
class="admin-modal-frame fixed inset-0 grid place-items-center bg-black/40 px-5 py-8"
:class="[zClass, rootClass]"
role="dialog"
aria-modal="true"
:aria-labelledby="props.labelledBy"
@click.self="emit('close')"
>
<section
class="admin-modal-frame__panel max-h-[90vh] w-full overflow-y-auto rounded border border-line bg-white p-5 shadow-xl"
:class="panelClass"
>
<slot />
</section>
</div>
</template>

View File

@@ -189,6 +189,7 @@
| components/admin/AdminMemberForm.vue | 관리자 멤버 추가/수정 폼(Ghost형 3분할, 기존 회원 보기/수정 모드 분리, 요약 1fr·입력 2fr, 원형 썸네일 hover 등록·변경·삭제, 기본 정보·레이블·관리자 노트·활동 요약, 설정 메뉴의 비밀번호 변경·멤버 삭제 모달, 저장 토스트, 미저장 변경사항 이탈 확인) | | components/admin/AdminMemberForm.vue | 관리자 멤버 추가/수정 폼(Ghost형 3분할, 기존 회원 보기/수정 모드 분리, 요약 1fr·입력 2fr, 원형 썸네일 hover 등록·변경·삭제, 기본 정보·레이블·관리자 노트·활동 요약, 설정 메뉴의 비밀번호 변경·멤버 삭제 모달, 저장 토스트, 미저장 변경사항 이탈 확인) |
| components/admin/AdminUnsavedChangesModal.vue | 관리자 편집 화면 공통 미저장 변경사항 이탈 확인 모달(상단 40px 위치) | | components/admin/AdminUnsavedChangesModal.vue | 관리자 편집 화면 공통 미저장 변경사항 이탈 확인 모달(상단 40px 위치) |
| components/admin/AdminConfirmModal.vue | 관리자 공통 확인 모달(제목·본문 슬롯·확인/취소 버튼, primary/danger 톤) | | components/admin/AdminConfirmModal.vue | 관리자 공통 확인 모달(제목·본문 슬롯·확인/취소 버튼, primary/danger 톤) |
| components/admin/AdminModalFrame.vue | 관리자 공통 일반 모달 프레임(오버레이, dialog 접근성 속성, 기본 패널 스타일, 바깥 클릭 닫기) |
| components/admin/AdminToast.vue | 관리자 공통 우측 상단 토스트 표시(성공·오류·안내 톤, 화면별 상단 위치 조정) | | components/admin/AdminToast.vue | 관리자 공통 우측 상단 토스트 표시(성공·오류·안내 톤, 화면별 상단 위치 조정) |
| components/admin/AdminSearchInput.vue | 관리자 공통 검색 입력(돋보기 아이콘, IME 검색 이벤트 전달, 외부 focus/select 노출) | | components/admin/AdminSearchInput.vue | 관리자 공통 검색 입력(돋보기 아이콘, IME 검색 이벤트 전달, 외부 focus/select 노출) |
| components/admin/AdminSelectInput.vue | 관리자 공통 select 입력(문자·숫자 옵션 값, 기본 필터·정렬 옵션, chevron 아이콘, `v-model` 전달) | | components/admin/AdminSelectInput.vue | 관리자 공통 select 입력(문자·숫자 옵션 값, 기본 필터·정렬 옵션, chevron 아이콘, `v-model` 전달) |

View File

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

View File

@@ -1,5 +1,10 @@
# 업데이트 이력 # 업데이트 이력
## v1.11.11
- 관리자 일반 모달 오버레이·패널을 공용 `AdminModalFrame` 컴포넌트로 분리.
- 미디어 폴더 생성과 네비게이션 생성·페이지 섹션·페이지 순서·추천 사이트 편집 모달의 반복 wrapper 제거.
## v1.11.10 ## v1.11.10
- 관리자 페이지네이션 계산 로직을 `lib/admin-pagination.js` 순수 함수로 분리. - 관리자 페이지네이션 계산 로직을 `lib/admin-pagination.js` 순수 함수로 분리.

4
package-lock.json generated
View File

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

View File

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

View File

@@ -1663,15 +1663,13 @@ watch(() => mediaPagination.value.page, (page) => {
</div> </div>
</div> </div>
<div <AdminModalFrame
v-if="isCreateFolderModalOpen" v-if="isCreateFolderModalOpen"
class="admin-media__folder-create-modal fixed inset-0 z-[60] grid place-items-center bg-black/40 px-5 py-8" labelled-by="media-folder-modal-title"
role="dialog" root-class="admin-media__folder-create-modal"
aria-modal="true" panel-class="admin-media__folder-create-panel max-w-sm"
aria-labelledby="media-folder-modal-title" @close="closeCreateFolderModal"
@click.self="closeCreateFolderModal"
> >
<section class="admin-media__folder-create-panel w-full max-w-sm rounded border border-line bg-white p-5 shadow-xl">
<h3 id="media-folder-modal-title" class="text-lg font-semibold text-ink"> <h3 id="media-folder-modal-title" class="text-lg font-semibold text-ink">
새 폴더 새 폴더
</h3> </h3>
@@ -1701,8 +1699,7 @@ watch(() => mediaPagination.value.page, (page) => {
만들기 만들기
</button> </button>
</div> </div>
</section> </AdminModalFrame>
</div>
<div <div
v-if="selectedMedia" v-if="selectedMedia"

View File

@@ -2101,15 +2101,13 @@ const saveNavigation = async () => {
</ul> </ul>
</div> </div>
<div <AdminModalFrame
v-if="navCreateModalType" v-if="navCreateModalType"
class="admin-navigation__create-modal fixed inset-0 z-[60] grid place-items-center bg-black/40 px-5 py-8" labelled-by="admin-navigation-create-title"
role="dialog" root-class="admin-navigation__create-modal"
aria-modal="true" panel-class="admin-navigation__create-modal-panel max-w-lg"
aria-labelledby="admin-navigation-create-title" @close="closeNavCreateModal"
@click.self="closeNavCreateModal"
> >
<section class="admin-navigation__create-modal-panel max-h-[90vh] w-full max-w-lg overflow-y-auto rounded border border-line bg-white p-5 shadow-xl">
<div class="admin-navigation__create-modal-head"> <div class="admin-navigation__create-modal-head">
<h3 id="admin-navigation-create-title" class="text-lg font-semibold text-ink"> <h3 id="admin-navigation-create-title" class="text-lg font-semibold text-ink">
{{ navCreateModalTitle }} {{ navCreateModalTitle }}
@@ -2179,18 +2177,15 @@ const saveNavigation = async () => {
추가 추가
</button> </button>
</div> </div>
</section> </AdminModalFrame>
</div>
<div <AdminModalFrame
v-if="pageSectionCreateModalOpen" v-if="pageSectionCreateModalOpen"
class="admin-navigation__page-section-create-modal fixed inset-0 z-[60] grid place-items-center bg-black/40 px-5 py-8" labelled-by="admin-navigation-page-section-create-title"
role="dialog" root-class="admin-navigation__page-section-create-modal"
aria-modal="true" panel-class="admin-navigation__page-section-create-modal-panel max-w-lg"
aria-labelledby="admin-navigation-page-section-create-title" @close="closePageSectionCreateModal"
@click.self="closePageSectionCreateModal"
> >
<section class="admin-navigation__page-section-create-modal-panel max-h-[90vh] w-full max-w-lg overflow-y-auto rounded border border-line bg-white p-5 shadow-xl">
<div class="admin-navigation__page-section-create-modal-head"> <div class="admin-navigation__page-section-create-modal-head">
<h3 id="admin-navigation-page-section-create-title" class="text-lg font-semibold text-ink"> <h3 id="admin-navigation-page-section-create-title" class="text-lg font-semibold text-ink">
페이지 섹션 추가 페이지 섹션 추가
@@ -2226,8 +2221,7 @@ const saveNavigation = async () => {
{{ savingPageSectionId === 'new' ? '추가 중' : '추가' }} {{ savingPageSectionId === 'new' ? '추가 중' : '추가' }}
</button> </button>
</div> </div>
</section> </AdminModalFrame>
</div>
<AdminConfirmModal <AdminConfirmModal
:open="recommendedDiscardModalOpen" :open="recommendedDiscardModalOpen"
@@ -2254,15 +2248,13 @@ const saveNavigation = async () => {
</p> </p>
</AdminConfirmModal> </AdminConfirmModal>
<div <AdminModalFrame
v-if="pageOrderModalSection" v-if="pageOrderModalSection"
class="admin-navigation__page-order-modal fixed inset-0 z-[60] grid place-items-center bg-black/40 px-5 py-8" labelled-by="admin-navigation-page-order-title"
role="dialog" root-class="admin-navigation__page-order-modal"
aria-modal="true" panel-class="admin-navigation__page-order-modal-panel max-w-4xl"
aria-labelledby="admin-navigation-page-order-title" @close="closePageOrderModal"
@click.self="closePageOrderModal"
> >
<section class="admin-navigation__page-order-modal-panel max-h-[90vh] w-full max-w-4xl overflow-y-auto rounded border border-line bg-white p-5 shadow-xl">
<div class="admin-navigation__page-order-modal-head flex items-start justify-between gap-4"> <div class="admin-navigation__page-order-modal-head flex items-start justify-between gap-4">
<div> <div>
<h3 id="admin-navigation-page-order-title" class="text-lg font-semibold text-ink"> <h3 id="admin-navigation-page-order-title" class="text-lg font-semibold text-ink">
@@ -2347,18 +2339,15 @@ const saveNavigation = async () => {
{{ savingPageOrder ? '저장 중' : '저장' }} {{ savingPageOrder ? '저장 중' : '저장' }}
</button> </button>
</div> </div>
</section> </AdminModalFrame>
</div>
<div <AdminModalFrame
v-if="recommendedEditId && recommendedEditDraft" v-if="recommendedEditId && recommendedEditDraft"
class="admin-navigation__recommended-modal fixed inset-0 z-[60] grid place-items-center bg-black/40 px-5 py-8" labelled-by="admin-navigation-recommended-edit-title"
role="dialog" root-class="admin-navigation__recommended-modal"
aria-modal="true" panel-class="admin-navigation__recommended-modal-panel max-w-lg"
aria-labelledby="admin-navigation-recommended-edit-title" @close="requestCloseRecommendedEdit"
@click.self="requestCloseRecommendedEdit"
> >
<section class="admin-navigation__recommended-modal-panel max-h-[90vh] w-full max-w-lg overflow-y-auto rounded border border-line bg-white p-5 shadow-xl">
<div class="admin-navigation__recommended-modal-head"> <div class="admin-navigation__recommended-modal-head">
<h3 id="admin-navigation-recommended-edit-title" class="text-lg font-semibold text-ink"> <h3 id="admin-navigation-recommended-edit-title" class="text-lg font-semibold text-ink">
추천 사이트 편집 추천 사이트 편집
@@ -2458,7 +2447,7 @@ const saveNavigation = async () => {
class="min-h-24 resize-y rounded border border-line px-3 py-2 font-mono text-sm outline-none focus:border-[#8e9cac]" class="min-h-24 resize-y rounded border border-line px-3 py-2 font-mono text-sm outline-none focus:border-[#8e9cac]"
placeholder="/uploads/…" placeholder="/uploads/…"
/> />
<span class="text-[11px] pl-2 text-muted">비워 두면 서버 캐시 파비콘을 사용합니다.</span> <span class="pl-2 text-[11px] text-muted">비워 두면 서버 캐시 파비콘을 사용합니다.</span>
</label> </label>
</div> </div>
@@ -2499,8 +2488,7 @@ const saveNavigation = async () => {
</button> </button>
</div> </div>
</div> </div>
</section> </AdminModalFrame>
</div>
<AdminToast :toast="toast" /> <AdminToast :toast="toast" />
</section> </section>