|
|
|
|
@@ -5,6 +5,25 @@ const isPostEditorRoute = computed(() => route.path === '/admin/posts/new'
|
|
|
|
|
|| (route.path.startsWith('/admin/posts/') && route.path !== '/admin/posts/preview'))
|
|
|
|
|
|
|
|
|
|
const editorDocumentClass = 'admin-post-editor-document'
|
|
|
|
|
const adminUserMenuOpen = ref(false)
|
|
|
|
|
|
|
|
|
|
const { data: adminMember } = await useFetch('/api/auth/me', {
|
|
|
|
|
default: () => ({
|
|
|
|
|
username: '',
|
|
|
|
|
email: '',
|
|
|
|
|
avatarUrl: ''
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const { data: adminMembers } = await useFetch('/admin/api/members', {
|
|
|
|
|
default: () => []
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const memberCount = computed(() => adminMembers.value.length)
|
|
|
|
|
const adminDisplayName = computed(() => adminMember.value?.username || adminMember.value?.email || '관리자')
|
|
|
|
|
const adminDisplayEmail = computed(() => adminMember.value?.email || '')
|
|
|
|
|
const adminAvatarUrl = computed(() => adminMember.value?.avatarUrl || '')
|
|
|
|
|
const adminAvatarInitial = computed(() => adminDisplayName.value.slice(0, 1).toUpperCase() || 'A')
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 관리자 내비게이션 활성 경로 확인
|
|
|
|
|
@@ -13,6 +32,39 @@ const editorDocumentClass = 'admin-post-editor-document'
|
|
|
|
|
*/
|
|
|
|
|
const isAdminNavActive = (path) => route.path === path || route.path.startsWith(`${path}/`)
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 관리자 사용자 메뉴 닫기
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
const closeAdminUserMenu = () => {
|
|
|
|
|
adminUserMenuOpen.value = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 관리자 사용자 메뉴 토글
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
const toggleAdminUserMenu = () => {
|
|
|
|
|
adminUserMenuOpen.value = !adminUserMenuOpen.value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 외부 클릭 시 관리자 사용자 메뉴 닫기
|
|
|
|
|
* @param {PointerEvent} event - 포인터 이벤트
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
const onAdminDocumentPointerDown = (event) => {
|
|
|
|
|
if (!adminUserMenuOpen.value || !(event.target instanceof HTMLElement)) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (event.target.closest('[data-admin-user-menu]')) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
closeAdminUserMenu()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 글쓰기 전체 화면 문서 스크롤 잠금 적용
|
|
|
|
|
* @returns {void}
|
|
|
|
|
@@ -28,6 +80,10 @@ const syncPostEditorDocumentClass = () => {
|
|
|
|
|
|
|
|
|
|
watchEffect(syncPostEditorDocumentClass)
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
document.addEventListener('pointerdown', onAdminDocumentPointerDown)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
if (!import.meta.client) {
|
|
|
|
|
return
|
|
|
|
|
@@ -35,6 +91,7 @@ onBeforeUnmount(() => {
|
|
|
|
|
|
|
|
|
|
document.documentElement.classList.remove(editorDocumentClass)
|
|
|
|
|
document.body.classList.remove(editorDocumentClass)
|
|
|
|
|
document.removeEventListener('pointerdown', onAdminDocumentPointerDown)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@@ -45,6 +102,7 @@ const logoutAdmin = async () => {
|
|
|
|
|
await $fetch('/admin/api/auth/logout', {
|
|
|
|
|
method: 'POST'
|
|
|
|
|
})
|
|
|
|
|
closeAdminUserMenu()
|
|
|
|
|
await navigateTo('/admin/login')
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
@@ -56,7 +114,7 @@ const logoutAdmin = async () => {
|
|
|
|
|
>
|
|
|
|
|
<aside
|
|
|
|
|
v-if="!isPostEditorRoute"
|
|
|
|
|
class="admin-layout__sidebar fixed inset-y-0 left-0 hidden w-80 border-r border-[#e6e8eb] bg-[#f7f8fa] px-5 py-6 text-[#15171a] lg:block"
|
|
|
|
|
class="admin-layout__sidebar fixed inset-y-0 left-0 hidden w-80 flex-col border-r border-[#e6e8eb] bg-[#f7f8fa] px-5 py-6 text-[#15171a] lg:flex"
|
|
|
|
|
>
|
|
|
|
|
<NuxtLink class="admin-layout__brand flex items-center gap-3 px-2 text-[0.95rem] font-semibold tracking-[-0.01em]" to="/admin">
|
|
|
|
|
<span class="admin-layout__brand-mark flex h-8 w-8 items-center justify-center rounded-full border border-[#d8dce1] bg-white shadow-[0_1px_2px_rgba(15,23,42,0.05)]">
|
|
|
|
|
@@ -71,7 +129,8 @@ const logoutAdmin = async () => {
|
|
|
|
|
>
|
|
|
|
|
<NuxtLink class="admin-layout__nav-link flex min-w-0 flex-1 items-center gap-3 px-3 py-2" to="/admin/posts">
|
|
|
|
|
<svg class="admin-layout__nav-icon h-4 w-4 shrink-0" viewBox="0 0 24 24" aria-hidden="true">
|
|
|
|
|
<path d="M5.5.75l10.72 10.72a.72.72 0 01.22.53.72.72 0 01-.22.53L5.5 23.25" stroke="#000" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round" />
|
|
|
|
|
<path d="M12.667 14.136l-3.712.531.53-3.713 9.546-9.546a2.25 2.25 0 013.182 3.182z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
|
|
|
|
<path d="M19.122 14.25v7.5a1.5 1.5 0 01-1.5 1.5h-15a1.5 1.5 0 01-1.5-1.5v-15a1.5 1.5 0 011.5-1.5h7.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
|
|
|
|
</svg>
|
|
|
|
|
<span class="truncate">게시글</span>
|
|
|
|
|
</NuxtLink>
|
|
|
|
|
@@ -116,9 +175,18 @@ const logoutAdmin = async () => {
|
|
|
|
|
</svg>
|
|
|
|
|
<span>미디어</span>
|
|
|
|
|
</NuxtLink>
|
|
|
|
|
<NuxtLink class="admin-layout__nav-link flex items-center gap-3 rounded-md px-3 py-2 transition-colors hover:bg-[#eceff2] hover:text-[#15171a]" to="/admin/navigation">
|
|
|
|
|
<span class="admin-layout__nav-icon h-4 w-4 shrink-0 rounded-full border border-current" aria-hidden="true" />
|
|
|
|
|
<span>메뉴</span>
|
|
|
|
|
<NuxtLink
|
|
|
|
|
class="admin-layout__nav-link flex items-center gap-3 rounded-md px-3 py-2 transition-colors hover:bg-[#eceff2] hover:text-[#15171a]"
|
|
|
|
|
:class="isAdminNavActive('/admin/navigation') ? 'bg-[#e9ecef] text-[#15171a]' : ''"
|
|
|
|
|
to="/admin/navigation"
|
|
|
|
|
>
|
|
|
|
|
<svg class="admin-layout__nav-icon h-4 w-4 shrink-0" xmlns="http://www.w3.org/2000/svg" viewBox="-0.75 -0.75 24 24" aria-hidden="true">
|
|
|
|
|
<path d="M2.109375 0.7059375h18.28125s1.40625 0 1.40625 1.40625v18.28125s0 1.40625 -1.40625 1.40625H2.109375s-1.40625 0 -1.40625 -1.40625v-18.28125s0 -1.40625 1.40625 -1.40625" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
|
|
|
|
<path d="m6.328125 7.0340625 9.84375 0" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
|
|
|
|
<path d="m6.328125 11.252812500000001 9.84375 0" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
|
|
|
|
<path d="m6.328125 15.471562500000001 9.84375 0" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
|
|
|
|
</svg>
|
|
|
|
|
<span>네비게이션</span>
|
|
|
|
|
</NuxtLink>
|
|
|
|
|
<NuxtLink
|
|
|
|
|
class="admin-layout__nav-link flex items-center gap-3 rounded-md px-3 py-2 transition-colors hover:bg-[#eceff2] hover:text-[#15171a]"
|
|
|
|
|
@@ -131,23 +199,64 @@ const logoutAdmin = async () => {
|
|
|
|
|
<circle cx="17.727" cy="10.125" r="3.375" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
|
|
|
|
<path d="M15.813 15.068a5.526 5.526 0 017.437 5.182" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
|
|
|
|
</svg>
|
|
|
|
|
<span>멤버</span>
|
|
|
|
|
<span class="min-w-0 flex-1">멤버</span>
|
|
|
|
|
<span class="admin-layout__member-count ml-auto rounded-full bg-[#e1e5e9] px-2 py-0.5 text-xs font-semibold text-[#5d6673]">
|
|
|
|
|
{{ memberCount }}
|
|
|
|
|
</span>
|
|
|
|
|
</NuxtLink>
|
|
|
|
|
<NuxtLink
|
|
|
|
|
class="admin-layout__nav-link flex items-center gap-3 rounded-md px-3 py-2 transition-colors hover:bg-[#eceff2] hover:text-[#15171a]"
|
|
|
|
|
:class="isAdminNavActive('/admin/settings') ? 'bg-[#e9ecef] text-[#15171a]' : ''"
|
|
|
|
|
to="/admin/settings"
|
|
|
|
|
>
|
|
|
|
|
<svg class="admin-layout__nav-icon h-4 w-4 shrink-0" viewBox="0 0 24 24" aria-hidden="true">
|
|
|
|
|
<path d="M10.546 2.438a1.957 1.957 0 002.908 0L14.4 1.4a1.959 1.959 0 013.41 1.413l-.071 1.4a1.958 1.958 0 002.051 2.054l1.4-.071a1.959 1.959 0 011.41 3.41l-1.042.94a1.96 1.96 0 000 2.909l1.042.94a1.959 1.959 0 01-1.413 3.41l-1.4-.071a1.958 1.958 0 00-2.056 2.056l.071 1.4A1.959 1.959 0 0114.4 22.6l-.941-1.041a1.959 1.959 0 00-2.908 0L9.606 22.6A1.959 1.959 0 016.2 21.192l.072-1.4a1.958 1.958 0 00-2.056-2.056l-1.4.071A1.958 1.958 0 011.4 14.4l1.041-.94a1.96 1.96 0 000-2.909L1.4 9.606A1.958 1.958 0 012.809 6.2l1.4.071a1.958 1.958 0 002.058-2.06L6.2 2.81A1.959 1.959 0 019.606 1.4z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
|
|
|
|
<circle cx="12" cy="12.001" r="4.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
|
|
|
|
</svg>
|
|
|
|
|
<span>설정</span>
|
|
|
|
|
</NuxtLink>
|
|
|
|
|
<button class="admin-layout__logout mt-4 rounded-md px-3 py-2 text-left text-[#8a929d] transition-colors hover:bg-[#eceff2] hover:text-[#15171a]" type="button" @click="logoutAdmin">
|
|
|
|
|
로그아웃
|
|
|
|
|
</button>
|
|
|
|
|
</nav>
|
|
|
|
|
<div class="admin-layout__bottom relative mt-auto pt-6" data-admin-user-menu>
|
|
|
|
|
<div
|
|
|
|
|
v-if="adminUserMenuOpen"
|
|
|
|
|
class="admin-layout__user-popover absolute bottom-14 left-0 right-0 overflow-hidden rounded-xl border border-[#e2e5e9] bg-white text-[#15171a] shadow-[0_18px_50px_rgba(15,23,42,0.16)]"
|
|
|
|
|
>
|
|
|
|
|
<div class="admin-layout__user-summary flex items-center gap-3 border-b border-[#eceff2] px-4 py-4">
|
|
|
|
|
<img
|
|
|
|
|
v-if="adminAvatarUrl"
|
|
|
|
|
class="admin-layout__user-avatar h-11 w-11 rounded-full border border-[#e2e5e9] object-cover"
|
|
|
|
|
:src="adminAvatarUrl"
|
|
|
|
|
:alt="adminDisplayName"
|
|
|
|
|
>
|
|
|
|
|
<span v-else class="admin-layout__user-avatar flex h-11 w-11 items-center justify-center rounded-full border border-[#e2e5e9] bg-[#15171a] text-sm font-semibold text-white">
|
|
|
|
|
{{ adminAvatarInitial }}
|
|
|
|
|
</span>
|
|
|
|
|
<span class="min-w-0">
|
|
|
|
|
<span class="block truncate text-sm font-semibold">{{ adminDisplayName }}</span>
|
|
|
|
|
<span class="block truncate text-xs text-[#657080]">{{ adminDisplayEmail }}</span>
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="admin-layout__user-actions grid py-2 text-sm text-[#3f4650]">
|
|
|
|
|
<NuxtLink class="admin-layout__user-action px-4 py-2.5 hover:bg-[#f3f5f7]" to="/settings" @click="closeAdminUserMenu">
|
|
|
|
|
내 프로필
|
|
|
|
|
</NuxtLink>
|
|
|
|
|
<button class="admin-layout__user-action px-4 py-2.5 text-left hover:bg-[#f3f5f7]" type="button" @click="logoutAdmin">
|
|
|
|
|
로그아웃
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="admin-layout__bottom-row flex items-center justify-between gap-3 px-2">
|
|
|
|
|
<button class="admin-layout__user-trigger flex min-w-0 items-center gap-2 rounded-md px-1.5 py-1.5 transition-colors hover:bg-[#eceff2]" type="button" :aria-expanded="adminUserMenuOpen" @click="toggleAdminUserMenu">
|
|
|
|
|
<img
|
|
|
|
|
v-if="adminAvatarUrl"
|
|
|
|
|
class="admin-layout__user-trigger-avatar h-8 w-8 rounded-full border border-[#d8dce1] object-cover"
|
|
|
|
|
:src="adminAvatarUrl"
|
|
|
|
|
:alt="adminDisplayName"
|
|
|
|
|
>
|
|
|
|
|
<span v-else class="admin-layout__user-trigger-avatar flex h-8 w-8 items-center justify-center rounded-full border border-[#d8dce1] bg-[#15171a] text-xs font-semibold text-white">
|
|
|
|
|
{{ adminAvatarInitial }}
|
|
|
|
|
</span>
|
|
|
|
|
<svg class="h-3 w-3 shrink-0 text-[#5d6673]" fill="none" viewBox="0 0 26 24" aria-hidden="true">
|
|
|
|
|
<path clip-rule="evenodd" d="M1.043 6.604a1 1 0 011.414 0L13 17.146 23.543 6.604a1 1 0 011.414 1.414l-10.72 10.719a1.75 1.75 0 01-2.474 0L1.042 8.018a1 1 0 010-1.414zm11.78 10.72v-.001zm.355 0v-.001z" fill="currentColor" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
<NuxtLink class="admin-layout__bottom-settings flex h-9 w-9 items-center justify-center rounded-md text-[#3f4650] transition-colors hover:bg-[#eceff2] hover:text-[#15171a]" to="/admin/settings" aria-label="설정">
|
|
|
|
|
<svg class="h-5 w-5" viewBox="0 0 24 24" aria-hidden="true">
|
|
|
|
|
<path d="M10.546 2.438a1.957 1.957 0 002.908 0L14.4 1.4a1.959 1.959 0 013.41 1.413l-.071 1.4a1.958 1.958 0 002.051 2.054l1.4-.071a1.959 1.959 0 011.41 3.41l-1.042.94a1.96 1.96 0 000 2.909l1.042.94a1.959 1.959 0 01-1.413 3.41l-1.4-.071a1.958 1.958 0 00-2.056 2.056l.071 1.4A1.959 1.959 0 0114.4 22.6l-.941-1.041a1.959 1.959 0 00-2.908 0L9.606 22.6A1.959 1.959 0 016.2 21.192l.072-1.4a1.958 1.958 0 00-2.056-2.056l-1.4.071A1.958 1.958 0 011.4 14.4l1.041-.94a1.96 1.96 0 000-2.909L1.4 9.606A1.958 1.958 0 012.809 6.2l1.4.071a1.958 1.958 0 002.058-2.06L6.2 2.81A1.959 1.959 0 019.606 1.4z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
|
|
|
|
<circle cx="12" cy="12.001" r="4.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
|
|
|
|
</svg>
|
|
|
|
|
</NuxtLink>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</aside>
|
|
|
|
|
<main
|
|
|
|
|
class="admin-layout__main bg-paper"
|
|
|
|
|
|