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) => {