메뉴 관리 개편, 추천 사이트·1뎁스 네비, 우측 Recommended 연동(v1.1.13)

상단 네비는 평면 테이블·드래그로 편집하고 한 단계 하위만 허용한다.
추천 사이트 탭·location recommended·공개 API와 우측 사이드 카드·파비콘 URL 유틸을 추가한다.
문서·배포 마이그레이션 안내·관리자 레이아웃·설정 화면 등 누적 변경을 반영한다.
This commit is contained in:
2026-05-15 14:20:27 +09:00
parent 2768975752
commit ca1e17890b
24 changed files with 1509 additions and 499 deletions

View File

@@ -1,12 +1,26 @@
<script setup>
const route = useRoute()
const runtimeConfig = useRuntimeConfig()
/**
* 공개 블로그 베이스 URL (후행 슬래시 제거, 새 창 링크용)
* @returns {string} 절대 URL
*/
const publicBlogBaseUrl = computed(() => {
const raw = String(runtimeConfig.public?.siteUrl || '').trim()
return raw.replace(/\/+$/, '') || 'https://sori.studio'
})
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 settingsDocumentClass = 'admin-settings-document'
const adminUserMenuOpen = ref(false)
const isAdminSettingsRoute = computed(() => route.path === '/admin/settings'
|| route.path.startsWith('/admin/settings/'))
const { data: adminMember } = await useFetch('/api/auth/me', {
default: () => ({
username: '',
@@ -67,19 +81,23 @@ const onAdminDocumentPointerDown = (event) => {
}
/**
* 글쓰기 전체 화면 문서 스크롤 잠금 적용
* 글쓰기·설정 전체 화면 문서 스크롤 잠금 적용
* @returns {void}
*/
const syncPostEditorDocumentClass = () => {
const syncAdminShellDocumentClass = () => {
if (!import.meta.client) {
return
}
document.documentElement.classList.toggle(editorDocumentClass, isPostEditorRoute.value)
document.body.classList.toggle(editorDocumentClass, isPostEditorRoute.value)
const editorOn = isPostEditorRoute.value
const settingsOn = isAdminSettingsRoute.value && !editorOn
document.documentElement.classList.toggle(editorDocumentClass, editorOn)
document.body.classList.toggle(editorDocumentClass, editorOn)
document.documentElement.classList.toggle(settingsDocumentClass, settingsOn)
document.body.classList.toggle(settingsDocumentClass, settingsOn)
}
watchEffect(syncPostEditorDocumentClass)
watchEffect(syncAdminShellDocumentClass)
onMounted(() => {
document.addEventListener('pointerdown', onAdminDocumentPointerDown)
@@ -92,6 +110,8 @@ onBeforeUnmount(() => {
document.documentElement.classList.remove(editorDocumentClass)
document.body.classList.remove(editorDocumentClass)
document.documentElement.classList.remove(settingsDocumentClass)
document.body.classList.remove(settingsDocumentClass)
document.removeEventListener('pointerdown', onAdminDocumentPointerDown)
})
@@ -111,10 +131,10 @@ const logoutAdmin = async () => {
<template>
<div
class="admin-layout bg-[#f7f8fa] text-ink"
:class="isPostEditorRoute ? 'h-screen overflow-hidden bg-white' : 'min-h-screen'"
:class="(isPostEditorRoute || isAdminSettingsRoute) ? 'h-screen overflow-hidden bg-white' : 'min-h-screen'"
>
<aside
v-if="!isPostEditorRoute"
v-if="!isPostEditorRoute && !isAdminSettingsRoute"
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">
@@ -124,6 +144,29 @@ const logoutAdmin = async () => {
<span>sori.studio</span>
</NuxtLink>
<nav class="admin-layout__nav mt-10 grid gap-1.5 text-sm font-medium text-[#5d6673]">
<div
class="admin-layout__nav-link admin-layout__nav-link--disabled flex cursor-not-allowed items-center gap-3 rounded-md px-3 py-2 text-[#9aa3ad] select-none"
aria-disabled="true"
title="준비 중"
>
<svg class="admin-layout__nav-icon h-4 w-4 shrink-0 opacity-60" viewBox="0 0 24 24" aria-hidden="true">
<path d="M22.272 23.247a.981.981 0 00.978-.978V9.747a1.181 1.181 0 00-.377-.8L12 .747l-10.873 8.2a1.181 1.181 0 00-.377.8v12.522a.981.981 0 00.978.978z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
</svg>
<span>대시보드</span>
</div>
<a
class="admin-layout__nav-link admin-layout__nav-link--external flex items-center gap-3 rounded-md px-3 py-2 text-[#5d6673] transition-colors hover:bg-[#eceff2] hover:text-[#15171a]"
:href="publicBlogBaseUrl"
target="_blank"
rel="noopener noreferrer"
>
<svg class="admin-layout__nav-icon h-4 w-4 shrink-0" viewBox="0 0 24 24" aria-hidden="true">
<rect x="1.5" y="1.497" width="21" height="21" rx="1.5" ry="1.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
<path d="M1.5 7.497h21m-13.5 15v-15" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
</svg>
<span>사이트 보기</span>
</a>
<div class="admin-layout__nav-divider h-6 shrink-0" aria-hidden="true" />
<div
class="admin-layout__nav-item group flex items-center rounded-md transition-colors"
:class="isAdminNavActive('/admin/posts') ? 'bg-[#e9ecef] text-[#15171a]' : 'hover:bg-[#eceff2] hover:text-[#15171a]'"
@@ -260,10 +303,12 @@ const logoutAdmin = async () => {
</div>
</aside>
<main
class="admin-layout__main bg-paper"
class="admin-layout__main"
:class="[
isPostEditorRoute ? 'h-screen overflow-hidden' : 'min-h-screen px-8 py-8 xl:px-12 xl:py-10',
{ 'lg:ml-80': !isPostEditorRoute }
isPostEditorRoute || isAdminSettingsRoute
? 'h-screen overflow-hidden bg-white'
: 'min-h-screen bg-paper px-8 py-8 xl:px-12 xl:py-10',
{ 'lg:ml-80': !isPostEditorRoute && !isAdminSettingsRoute }
]"
>
<slot />