From 180213f6ed331bdabced89c4eb0441993c957c03 Mon Sep 17 00:00:00 2001
From: zenn
Date: Thu, 23 Jul 2026 16:25:59 +0900
Subject: [PATCH] =?UTF-8?q?=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EB=AA=A9?=
=?UTF-8?q?=EB=A1=9D=20=ED=96=89=20=EB=A9=94=EB=89=B4=20=EA=B3=B5=ED=86=B5?=
=?UTF-8?q?=ED=99=94?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/todo.md | 2 +-
docs/update.md | 5 +
package-lock.json | 4 +-
package.json | 2 +-
pages/admin/posts/index.vue | 191 +++++++-----------------------------
5 files changed, 44 insertions(+), 160 deletions(-)
diff --git a/docs/todo.md b/docs/todo.md
index 9bb0eb0..b4b2374 100644
--- a/docs/todo.md
+++ b/docs/todo.md
@@ -9,7 +9,7 @@
- `components/admin/AdminPostForm.vue`: 태그 입력, 대표 이미지 선택, 발행/초안 저장 상태, 우측 설정 패널 책임을 분리.
- `pages/admin/media/index.vue`: 미디어 목록, 폴더 트리, 상세 모달, 재생성 액션을 작은 단위로 분리.
- 관리자 UI 공통 컴포넌트 정리.
- - 모달 패널, 필터 팝오버, 행 메뉴, 페이지네이션 툴바의 radius·shadow·z-index·색상 규칙을 공통화.
+ - 모달 패널, 필터 팝오버, 페이지네이션 툴바의 radius·shadow·z-index·색상 규칙을 공통화.
- 게시글/페이지/미디어 목록의 검색·필터·정렬 레이아웃을 같은 패턴으로 유지.
- 최근 브라우저 회귀 테스트 추가.
- 태그 추천 선택 후 다음 태그 후보가 포커스 해제 없이 유지되는지 확인.
diff --git a/docs/update.md b/docs/update.md
index 4d409f5..8a755f4 100644
--- a/docs/update.md
+++ b/docs/update.md
@@ -1,5 +1,10 @@
# 업데이트 이력
+## v1.11.4
+
+- 관리자 게시글 목록 행 메뉴를 공용 `AdminRowMoreMenu`와 `useAdminRowMenu` 기준으로 통일.
+- 게시글 목록의 중복 행 메뉴 위치 계산·Teleport 렌더링 로직 제거.
+
## v1.11.3
- v1.11 내부 개선 TODO 추가.
diff --git a/package-lock.json b/package-lock.json
index e315fa9..d750050 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "sori.studio",
- "version": "1.11.3",
+ "version": "1.11.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sori.studio",
- "version": "1.11.3",
+ "version": "1.11.4",
"hasInstallScript": true,
"dependencies": {
"@nuxtjs/tailwindcss": "^6.14.0",
diff --git a/package.json b/package.json
index b0bb2a3..34a53c1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "sori.studio",
- "version": "1.11.3",
+ "version": "1.11.4",
"private": true,
"type": "module",
"imports": {
diff --git a/pages/admin/posts/index.vue b/pages/admin/posts/index.vue
index a2e1222..018f615 100644
--- a/pages/admin/posts/index.vue
+++ b/pages/admin/posts/index.vue
@@ -6,8 +6,7 @@ definePageMeta({
const deletingId = ref('')
const togglingFeaturedId = ref('')
const togglingPinnedId = ref('')
-const openPostMenuId = ref('')
-const postMenuStyle = ref({})
+const { openMenuId: openPostMenuId, closeMenu: closePostMenu } = useAdminRowMenu()
const errorMessage = ref('')
const statusFilter = ref('all')
const tagFilter = ref('all')
@@ -31,7 +30,6 @@ const referenceModalData = ref(createDefaultPostReferences())
const referenceModalLoading = ref(false)
const referenceModalError = ref('')
const referenceModalSearchQuery = ref('')
-const postMenuTriggerRefs = new Map()
const tagFilterRef = ref(null)
const postSearchInputRef = ref(null)
@@ -715,14 +713,6 @@ const selectAnalyticsModalPeriod = async (period) => {
await fetchAnalyticsModalData()
}
-/**
- * 열린 행 메뉴를 닫는다.
- * @returns {void}
- */
-const closePostMenu = () => {
- openPostMenuId.value = ''
-}
-
/**
* 태그 필터 드롭다운을 연다.
* @returns {void}
@@ -826,79 +816,14 @@ const setPostPage = (page) => {
}
/**
- * 게시물 행 메뉴 트리거 엘리먼트를 저장한다.
- * @param {string} postId - 게시물 ID
- * @param {Element|null} element - 트리거 엘리먼트
- * @returns {void}
- */
-const setPostMenuTriggerRef = (postId, element) => {
- if (!element) {
- postMenuTriggerRefs.delete(postId)
- return
- }
-
- postMenuTriggerRefs.set(postId, element)
-}
-
-/**
- * 게시물 행 메뉴 위치를 화면 기준으로 계산한다.
- * @returns {void}
- */
-const updatePostMenuPosition = () => {
- if (!import.meta.client || !openPostMenuId.value) {
- return
- }
-
- const trigger = postMenuTriggerRefs.get(openPostMenuId.value)
- if (!trigger) {
- return
- }
-
- const rect = trigger.getBoundingClientRect()
- const menuWidth = 176
- const estimatedHeight = 112
- const margin = 8
- const left = Math.max(margin, Math.min(rect.right - menuWidth, window.innerWidth - menuWidth - margin))
- const opensUp = rect.bottom + estimatedHeight + margin > window.innerHeight
- const top = opensUp
- ? Math.max(margin, rect.top - estimatedHeight - 4)
- : rect.bottom + 4
-
- postMenuStyle.value = {
- left: `${left}px`,
- top: `${top}px`,
- width: `${menuWidth}px`
- }
-}
-
-/**
- * 행 메뉴 열기/닫기
- * @param {string} postId - 게시물 ID
- * @returns {void}
- */
-const togglePostMenu = (postId) => {
- openPostMenuId.value = openPostMenuId.value === postId ? '' : postId
-}
-
-/**
- * 외부 클릭 시 행 메뉴 닫기
+ * 외부 클릭 시 태그 필터를 닫는다.
* @param {PointerEvent} event - 포인터 이벤트
* @returns {void}
*/
-const onPostMenuDocumentPointerDown = (event) => {
+const onPostListDocumentPointerDown = (event) => {
if (isTagFilterOpen.value && event.target instanceof Node && !tagFilterRef.value?.contains(event.target)) {
closeTagFilter()
}
-
- if (!openPostMenuId.value || !(event.target instanceof HTMLElement)) {
- return
- }
-
- if (event.target.closest('[data-admin-post-row-menu]')) {
- return
- }
-
- closePostMenu()
}
/**
@@ -1050,26 +975,13 @@ const deletePost = async (post) => {
}
onMounted(() => {
- document.addEventListener('pointerdown', onPostMenuDocumentPointerDown)
+ document.addEventListener('pointerdown', onPostListDocumentPointerDown)
document.addEventListener('keydown', onDocumentKeydown)
- window.addEventListener('resize', updatePostMenuPosition)
- window.addEventListener('scroll', updatePostMenuPosition, true)
})
onBeforeUnmount(() => {
- document.removeEventListener('pointerdown', onPostMenuDocumentPointerDown)
+ document.removeEventListener('pointerdown', onPostListDocumentPointerDown)
document.removeEventListener('keydown', onDocumentKeydown)
- window.removeEventListener('resize', updatePostMenuPosition)
- window.removeEventListener('scroll', updatePostMenuPosition, true)
-})
-
-watch(openPostMenuId, async (postId) => {
- if (!postId) {
- return
- }
-
- await nextTick()
- updatePostMenuPosition()
})
watch(tagFilterSearch, () => {
@@ -1385,74 +1297,41 @@ watch(postPageCount, (pageCount) => {
-
+
+
+
|