diff --git a/components/admin/AdminRowMoreMenu.vue b/components/admin/AdminRowMoreMenu.vue
new file mode 100644
index 0000000..119c21f
--- /dev/null
+++ b/components/admin/AdminRowMoreMenu.vue
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+
diff --git a/composables/useAdminRowMenu.js b/composables/useAdminRowMenu.js
new file mode 100644
index 0000000..15826a6
--- /dev/null
+++ b/composables/useAdminRowMenu.js
@@ -0,0 +1,45 @@
+/**
+ * 관리자 테이블·목록 행 more vert 메뉴 열림 상태
+ * @returns {{ openMenuId: import('vue').Ref, closeMenu: () => void }}
+ */
+export const useAdminRowMenu = () => {
+ const openMenuId = ref('')
+
+ /**
+ * 열린 메뉴를 닫는다.
+ * @returns {void}
+ */
+ const closeMenu = () => {
+ openMenuId.value = ''
+ }
+
+ /**
+ * 문서 바깥 클릭 시 메뉴를 닫는다.
+ * @param {PointerEvent} event - 포인터 이벤트
+ * @returns {void}
+ */
+ const onDocumentPointerDown = (event) => {
+ if (!openMenuId.value || !(event.target instanceof HTMLElement)) {
+ return
+ }
+
+ if (event.target.closest('[data-admin-row-menu]')) {
+ return
+ }
+
+ closeMenu()
+ }
+
+ onMounted(() => {
+ document.addEventListener('pointerdown', onDocumentPointerDown)
+ })
+
+ onBeforeUnmount(() => {
+ document.removeEventListener('pointerdown', onDocumentPointerDown)
+ })
+
+ return {
+ openMenuId,
+ closeMenu
+ }
+}
diff --git a/docs/map.md b/docs/map.md
index 6dca1af..e3d180a 100644
--- a/docs/map.md
+++ b/docs/map.md
@@ -113,16 +113,18 @@
|------|------|
| pages/admin/index.vue | 대시보드 |
| pages/admin/login.vue | 관리자 로그인(이메일·비밀번호 모두 입력 시에만 제출 버튼 활성) |
-| pages/admin/posts/index.vue | 글 목록, 헤더 우측에 필터(상태·태그·정렬)와 «새 글»을 한 줄 배치, 예약/초안/발행 텍스트 상태, 제목 옆 댓글 수, 읽기 전용 태그 배지, 삭제는 휴지통 아이콘 |
+| pages/admin/posts/index.vue | 글 목록, 헤더 우측에 필터(상태·태그·정렬)와 «새 글»을 한 줄 배치, 예약/초안/발행 텍스트 상태, 제목 옆 댓글 수, 읽기 전용 태그 배지, 행 끝 more vert 메뉴(추천·삭제) |
| pages/admin/posts/new.vue | 글 작성, Ghost 스타일 작성 폼, 초안 `POST` 디바운스 자동 저장·이탈 직전 플러시, 저장 토스트 |
| pages/admin/posts/[id].vue | 글 수정, `AdminPostForm`, 저장·초안 디바운스 자동 저장(`autoSaving`)·이탈 직전 플러시·삭제·토스트 |
| pages/admin/posts/preview.vue | 글 미리보기(공개 상세와 동일한 중앙 컬럼·수평 패딩) |
-| pages/admin/pages/index.vue | 페이지 목록 |
+| pages/admin/pages/index.vue | 페이지 목록, 행 more vert 메뉴(수정·삭제) |
| pages/admin/pages/new.vue | 페이지 작성, 저장 토스트 |
| pages/admin/pages/[id].vue | 페이지 수정, 저장/삭제 토스트 |
-| pages/admin/media/index.vue | 미디어 관리, **미디어 라이브러리/썸네일** 탭, 검색은 파일명·게시물/페이지 사용처 제목만, 라이브러리: 폴더 트리·드래그 이동 등, 현재 사이트 설정 로고·파비콘은 사용 중 파일로 잠금, 썸네일: 미참조 파일 삭제·이름 변경 가능, 상세 모달(연결 회원·폴더 편집·**다운로드**) |
-| pages/admin/navigation/index.vue | 메뉴 관리: 상단/하단/추천 사이트 탭, 상단은 1뎁스 제한·평면 테이블+계층형 개요·행 드래그(위/중/아래), 하단·추천은 평면 드래그, `useAdminToast` |
-| pages/admin/tags/index.vue | 태그 관리(메인 태그 드래그 정렬 자동 저장·일반 강등, 일반 태그 검색/메인 전환/삭제, 일반 태그 헤더의 태그 추가 버튼), 액션 피드백 토스트 |
+| pages/admin/media/index.vue | 미디어 관리, **미디어 라이브러리/썸네일** 탭, 폴더 트리 more vert(폴더 삭제), 검색·드래그·상세 모달 등 |
+| pages/admin/navigation/index.vue | 메뉴 관리: 상단/하단/추천 탭, 행 more vert(메뉴 항목 삭제), 드래그 정렬, `useAdminToast` |
+| components/admin/AdminRowMoreMenu.vue | 관리자 행 more vert 메뉴(트리거·팝오버) |
+| composables/useAdminRowMenu.js | 관리자 행 메뉴 열림 상태·바깥 클릭 닫기 |
+| pages/admin/tags/index.vue | 태그 관리(메인 태그 드래그 정렬 자동 저장·more vert 메뉴, 일반 태그 배지 more vert 메뉴·검색/정렬, 태그 추가 버튼), 액션 피드백 토스트 |
| pages/admin/tags/new.vue | 태그 생성 |
| pages/admin/tags/[id].vue | 태그 수정 |
| pages/admin/settings/index.vue | 사이트 설정 Ghost형 전체 화면, **POST 설정**(`showPostUpdatedAt` 수정일 표시 토글·저장), 블로그 제목·설명·기타(로고·URL·저작권), **메인 화면**(커버 이미지·오버레이 텍스트), 타임존·어나운스·Import/Export·스팸 플레이스홀더 |
diff --git a/docs/spec.md b/docs/spec.md
index e89612a..c5fe246 100644
--- a/docs/spec.md
+++ b/docs/spec.md
@@ -448,7 +448,7 @@ components/content/
- `PUT /admin/api/members/:id/role` - 회원 권한 변경(`owner`/`admin`/`member`)
> 글 발행/초안 전환은 `PUT /admin/api/posts/:id`의 `status`와 `published_at`으로 처리한다. 예약 글은 별도 enum이 아니라 `published`와 미래 시각의 `published_at` 조합이다.
-> 관리자 글 목록의 삭제 액션은 휴지통 아이콘으로 표시하며, 기본은 낮은 강조로 두고 호버 시에만 색·불투명도를 올린다.
+> 관리자 글 목록 맨 오른쪽 **관리** 열은 more vert(⋮) 버튼으로 행 메뉴를 연다. 메뉴에서 **게시글 추천**·**추천 제거**·**게시글 삭제**를 선택한다(사이드바 사용자 메뉴와 같은 팝오버 스타일).
> 관리자 글 목록 상단은 좌측에 제목·**총 N개**(추천 M개·필터 시 표시 K개) 요약, 우측에 상태·태그·**추천(전체/추천만)**·정렬 필터와 «새 글» 버튼을 한 줄(좁은 화면에서는 줄바꿈)로 두며 필터는 «새 글» 바로 왼쪽에 배치한다.
> 관리자 글 목록 표 첫 열은 `is_featured`(추천 글)일 때만 금색 별 아이콘을 표시한다. 추천 여부는 글 편집 설정의 «추천 글» 토글로 지정한다.
> 관리자 글 목록의 날짜 열은 **발행일**(`published_at`, 시·분 포함)이며, `showPostUpdatedAt`이 true이고 발행 후 수정이 있으면 아래에 `수정: …` 보조 줄을 표시한다.
diff --git a/docs/update.md b/docs/update.md
index e8b4a70..9b334df 100644
--- a/docs/update.md
+++ b/docs/update.md
@@ -5,6 +5,9 @@
- 홈 상단: Ghost형 헤딩·구독 폼 제거. 사이트 설정「메인 화면」에서 커버 이미지(720px)·오버레이 제목·본문 설정. `HomeHero.vue`, 마이그레이션 `027_site_settings_home_cover.sql`.
- 메인 화면 설정: 커버 업로드 시 제목·본문이 리셋되지 않도록 업로드는 파일만·저장 버튼에서 이미지·텍스트 일괄 `PUT` 반영.
- 테마 깜빡임: head 인라인 스크립트로 `data-theme` 선적용. 로딩 스플래시(`app.html`·캐시된 사이트 로고).
+- 관리자 글 목록: 휴지통 대신 more vert 행 메뉴(추천·추천 제거·삭제).
+- 관리자 태그 관리: 메인·일반 태그 모두 more vert 메뉴(수정·전환·제외·삭제).
+- 관리자 공통 `AdminRowMoreMenu`·`useAdminRowMenu`: 페이지·미디어 폴더·네비게이션 삭제 UI 통일. 태그 메뉴 항목 좌측 정렬 수정.
- 홈 Latest 피드: List(썸네일+본문)·Compact(텍스트만)·Cards(2열) 보기 구분. 메뉴 List/Compact 선택값과 레이아웃 일치. Default 클릭 시 Compact로 전환. Cards 상단 여백·테두리 클리핑 수정.
- 게시물 카드: 대표 이미지 없을 때 썸네일 영역에 제목 텍스트 플레이스홀더(`PostCardMedia`). 홈 Latest·태그·게시물 목록 공통.
- 슬래시 메뉴: 키보드 ↓ 이동 시 scrollIntoView+mouseenter 충돌로 하단 항목이 반복 선택되던 문제 수정.
diff --git a/pages/admin/media/index.vue b/pages/admin/media/index.vue
index 26cbdb9..c17db94 100644
--- a/pages/admin/media/index.vue
+++ b/pages/admin/media/index.vue
@@ -30,6 +30,15 @@ const draggingUrls = ref([])
const { toast, showToast } = useAdminToast()
+const { openMenuId, closeMenu } = useAdminRowMenu()
+
+/**
+ * 폴더 행 메뉴 id
+ * @param {string} folder - 폴더 경로
+ * @returns {string}
+ */
+const getFolderMenuId = (folder) => `folder:${folder}`
+
const { data: mediaItems, refresh } = await useFetch('/admin/api/media', {
default: () => []
})
@@ -332,6 +341,8 @@ const submitCreateFolderModal = async () => {
* @returns {Promise}
*/
const removeMediaFolder = async (folder) => {
+ closeMenu()
+
if (!folder || folder === '미분류' || folder === MEDIA_THUMBNAIL_ROOT || folder.startsWith(`${MEDIA_THUMBNAIL_ROOT}/`)) {
return
}
@@ -602,18 +613,26 @@ const deleteMedia = async (item) => {
{{ getFolderName(folder) }}
{{ folderMediaCounts[folder] || 0 }}
-
+
+
diff --git a/pages/admin/navigation/index.vue b/pages/admin/navigation/index.vue
index 0095e9e..09fba04 100644
--- a/pages/admin/navigation/index.vue
+++ b/pages/admin/navigation/index.vue
@@ -9,6 +9,8 @@ const saving = ref(false)
const activeTab = ref('primary')
const { toast, showToast, clearToast } = useAdminToast()
+const { openMenuId, closeMenu } = useAdminRowMenu()
+
const { data: navigationItems } = await useFetch('/admin/api/navigation', {
default: () => []
})
@@ -118,6 +120,16 @@ const removeItemCascade = (rootId) => {
items.value = items.value.filter((i) => !toRemove.has(String(i.id)))
}
+/**
+ * 메뉴 항목을 삭제한다(하위 포함).
+ * @param {string} itemId - 항목 id
+ * @returns {void}
+ */
+const removeNavItem = (itemId) => {
+ closeMenu()
+ removeItemCascade(itemId)
+}
+
/**
* 항목 id로 레코드를 찾는다.
* @param {string} id - 항목 id
@@ -750,7 +762,7 @@ const saveNavigation = async () => {
상단 메뉴가 없습니다. 위 버튼으로 항목을 추가하세요.
-
+
@@ -763,8 +775,8 @@ const saveNavigation = async () => {
|
URL
|
-
- 관리
+ |
+ 관리
|
@@ -817,14 +829,21 @@ const saveNavigation = async () => {
required
>
-
-
+
+
|
@@ -847,7 +866,7 @@ const saveNavigation = async () => {
하단 메뉴가 없습니다.
-