관리자 목록 more vert 메뉴 통일 및 태그 메뉴 정렬 수정
AdminRowMoreMenu 공통 컴포넌트로 글·태그·페이지·미디어·네비게이션 행 액션을 ⋮ 팝오버로 통일. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,9 +1,13 @@
|
||||
<script setup>
|
||||
import { toAdminPostStoredTitle } from '~/lib/admin-post-title.js'
|
||||
|
||||
definePageMeta({
|
||||
layout: 'admin'
|
||||
})
|
||||
|
||||
const deletingId = ref('')
|
||||
const togglingFeaturedId = ref('')
|
||||
const openPostMenuId = ref('')
|
||||
const errorMessage = ref('')
|
||||
const statusFilter = ref('all')
|
||||
const tagFilter = ref('all')
|
||||
@@ -172,13 +176,95 @@ const filteredPosts = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* 게시물 수정 API 본문을 구성한다.
|
||||
* @param {Object} post - 게시물
|
||||
* @param {Object} [overrides] - 덮어쓸 필드
|
||||
* @returns {Object} PUT 본문
|
||||
*/
|
||||
const buildPostUpdateBody = (post, overrides = {}) => ({
|
||||
title: toAdminPostStoredTitle(post.title),
|
||||
slug: post.slug,
|
||||
content: post.content ?? '',
|
||||
excerpt: post.excerpt ?? '',
|
||||
featuredImage: post.featuredImage ?? null,
|
||||
isFeatured: overrides.isFeatured ?? Boolean(post.isFeatured),
|
||||
seoTitle: post.seoTitle ?? '',
|
||||
seoDescription: post.seoDescription ?? '',
|
||||
canonicalUrl: post.canonicalUrl ?? '',
|
||||
noindex: Boolean(post.noindex),
|
||||
ogImage: post.ogImage ?? null,
|
||||
status: post.status,
|
||||
publishedAt: post.publishedAt,
|
||||
tags: post.tags ?? []
|
||||
})
|
||||
|
||||
/**
|
||||
* 열린 행 메뉴를 닫는다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const closePostMenu = () => {
|
||||
openPostMenuId.value = ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 행 메뉴 열기/닫기
|
||||
* @param {string} postId - 게시물 ID
|
||||
* @returns {void}
|
||||
*/
|
||||
const togglePostMenu = (postId) => {
|
||||
openPostMenuId.value = openPostMenuId.value === postId ? '' : postId
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 클릭 시 행 메뉴 닫기
|
||||
* @param {PointerEvent} event - 포인터 이벤트
|
||||
* @returns {void}
|
||||
*/
|
||||
const onPostMenuDocumentPointerDown = (event) => {
|
||||
if (!openPostMenuId.value || !(event.target instanceof HTMLElement)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (event.target.closest('[data-admin-post-row-menu]')) {
|
||||
return
|
||||
}
|
||||
|
||||
closePostMenu()
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시물 추천 여부를 토글한다.
|
||||
* @param {Object} post - 게시물
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const togglePostFeatured = async (post) => {
|
||||
closePostMenu()
|
||||
togglingFeaturedId.value = post.id
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
await $fetch(`/admin/api/posts/${post.id}`, {
|
||||
method: 'PUT',
|
||||
body: buildPostUpdateBody(post, { isFeatured: !post.isFeatured })
|
||||
})
|
||||
await refresh()
|
||||
} catch (error) {
|
||||
errorMessage.value = error?.data?.message || '추천 설정을 변경하지 못했습니다.'
|
||||
} finally {
|
||||
togglingFeaturedId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시물 삭제
|
||||
* @param {Object} post - 삭제할 게시물
|
||||
* @returns {Promise<void>} 삭제 처리 결과
|
||||
*/
|
||||
const deletePost = async (post) => {
|
||||
if (!confirm(`"${post.title}" 글을 삭제할까요?`)) {
|
||||
closePostMenu()
|
||||
|
||||
if (!confirm(`"${post.title || '(제목 없음)'}" 글을 삭제할까요?`)) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -196,6 +282,14 @@ const deletePost = async (post) => {
|
||||
deletingId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('pointerdown', onPostMenuDocumentPointerDown)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('pointerdown', onPostMenuDocumentPointerDown)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -265,7 +359,7 @@ const deletePost = async (post) => {
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
|
||||
<div class="admin-posts__table mt-8 overflow-hidden border border-line">
|
||||
<div class="admin-posts__table mt-8 overflow-x-auto border border-line">
|
||||
<table class="admin-posts__table-inner w-full border-collapse text-left text-sm">
|
||||
<thead class="admin-posts__table-head bg-[#f5f5f2] text-xs uppercase text-muted">
|
||||
<tr>
|
||||
@@ -277,7 +371,9 @@ const deletePost = async (post) => {
|
||||
<th class="admin-posts__cell px-4 py-3">상태</th>
|
||||
<th class="admin-posts__cell px-4 py-3">태그</th>
|
||||
<th class="admin-posts__cell px-4 py-3">발행일</th>
|
||||
<th class="admin-posts__cell px-4 py-3">관리</th>
|
||||
<th class="admin-posts__cell admin-posts__cell-actions w-12 px-2 py-3 text-right">
|
||||
<span class="sr-only">관리</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="admin-posts__table-body divide-y divide-line bg-white">
|
||||
@@ -335,26 +431,61 @@ const deletePost = async (post) => {
|
||||
{{ getUpdatedDateLabel(post) }}
|
||||
</p>
|
||||
</td>
|
||||
<td class="admin-posts__cell px-4 py-4">
|
||||
<button
|
||||
class="admin-posts__delete-icon inline-flex size-9 items-center justify-center rounded text-muted opacity-35 transition-all hover:opacity-100 hover:text-red-600 focus-visible:outline focus-visible:ring-2 focus-visible:ring-[#8e9cac] focus-visible:ring-offset-1 disabled:pointer-events-none disabled:opacity-20"
|
||||
type="button"
|
||||
:disabled="deletingId === post.id"
|
||||
:aria-label="deletingId === post.id ? '삭제 중' : '삭제'"
|
||||
@click="deletePost(post)"
|
||||
<td class="admin-posts__cell admin-posts__cell-actions relative w-12 px-2 py-4 text-right">
|
||||
<div
|
||||
class="admin-posts__row-menu relative inline-flex justify-end"
|
||||
data-admin-post-row-menu
|
||||
>
|
||||
<span v-if="deletingId === post.id" class="admin-posts__delete-progress text-[10px] font-semibold text-muted" aria-hidden="true">…</span>
|
||||
<svg
|
||||
v-else
|
||||
class="size-5 shrink-0"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
<button
|
||||
class="admin-posts__row-menu-trigger inline-flex size-9 items-center justify-center rounded text-[#394047] transition-colors hover:bg-[#eceff2] focus-visible:outline focus-visible:ring-2 focus-visible:ring-[#8e9cac] focus-visible:ring-offset-1 disabled:pointer-events-none disabled:opacity-40"
|
||||
type="button"
|
||||
:disabled="deletingId === post.id || togglingFeaturedId === post.id"
|
||||
:aria-expanded="openPostMenuId === post.id"
|
||||
aria-haspopup="menu"
|
||||
:aria-label="openPostMenuId === post.id ? '게시글 메뉴 닫기' : '게시글 메뉴'"
|
||||
@click.stop="togglePostMenu(post.id)"
|
||||
>
|
||||
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" />
|
||||
</svg>
|
||||
</button>
|
||||
<span
|
||||
v-if="deletingId === post.id || togglingFeaturedId === post.id"
|
||||
class="text-[10px] font-semibold text-muted"
|
||||
aria-hidden="true"
|
||||
>…</span>
|
||||
<svg
|
||||
v-else
|
||||
class="size-6 shrink-0"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M480-160q-33 0-56.5-23.5T400-240q0-33 23.5-56.5T480-320q33 0 56.5 23.5T560-240q0 33-23.5 56.5T480-160Zm0-240q-33 0-56.5-23.5T400-480q0-33 23.5-56.5T480-560q33 0 56.5 23.5T560-480q0 33-23.5 56.5T480-400Zm0-240q-33 0-56.5-23.5T400-720q0-33 23.5-56.5T480-800q33 0 56.5 23.5T560-720q0 33-23.5 56.5T480-640Z" />
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
v-if="openPostMenuId === post.id"
|
||||
class="admin-posts__row-menu-popover absolute right-0 top-full z-30 mt-1 min-w-[11rem] 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)]"
|
||||
role="menu"
|
||||
>
|
||||
<button
|
||||
class="admin-posts__row-menu-item block w-full px-4 py-2.5 text-left hover:bg-[#f3f5f7] disabled:opacity-50"
|
||||
type="button"
|
||||
role="menuitem"
|
||||
:disabled="togglingFeaturedId === post.id"
|
||||
@click="togglePostFeatured(post)"
|
||||
>
|
||||
{{ post.isFeatured ? '추천 제거' : '게시글 추천' }}
|
||||
</button>
|
||||
<button
|
||||
class="admin-posts__row-menu-item block w-full px-4 py-2.5 text-left text-[#c0392b] hover:bg-[#fef2f2] disabled:opacity-50"
|
||||
type="button"
|
||||
role="menuitem"
|
||||
:disabled="deletingId === post.id"
|
||||
@click="deletePost(post)"
|
||||
>
|
||||
게시글 삭제
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user