v1.3.2: 어나운스 바 슬라이드·설정 내비 아이콘
어나운스 바는 숨김 확인 후 슬라이드 인/아웃하고 7일간 보지 않기를 지원한다. 설정 좌측 내비에 타임존·메인 화면·어나운스·Import/Export·스팸 필터 아이콘을 추가한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
* 관리자 사이트 설정 좌측 내비 아이콘
|
||||
* @property {string} [iconId] - 아이콘 식별자. 미지정·미구현 시 자리 표시(placeholder)만 렌더
|
||||
*/
|
||||
const props = defineProps({
|
||||
defineProps({
|
||||
iconId: {
|
||||
type: String,
|
||||
default: ''
|
||||
@@ -17,6 +17,7 @@ const props = defineProps({
|
||||
:class="iconId ? `admin-settings-nav-icon--${iconId}` : 'admin-settings-nav-icon--placeholder'"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<!-- 블로그 제목·설명 -->
|
||||
<svg
|
||||
v-if="iconId === 'title-desc'"
|
||||
class="admin-settings-nav-icon__svg pointer-events-none size-4"
|
||||
@@ -24,35 +25,83 @@ const props = defineProps({
|
||||
viewBox="-0.75 -0.75 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M2.109375 6.32625h18.28125s1.40625 0 1.40625 1.40625v7.03125s0 1.40625 -1.40625 1.40625H2.109375s-1.40625 0 -1.40625 -1.40625v-7.03125s0 -1.40625 1.40625 -1.40625"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<path
|
||||
d="m16.171875 17.57625 0 -12.65625"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M11.953125 21.795a4.21875 4.21875 0 0 0 4.21875 -4.21875 4.21875 4.21875 0 0 0 4.21875 4.21875"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M11.953125 0.70125a4.21875 4.21875 0 0 1 4.21875 4.21875 4.21875 4.21875 0 0 1 4.21875 -4.21875"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<path d="M2.109375 6.32625h18.28125s1.40625 0 1.40625 1.40625v7.03125s0 1.40625 -1.40625 1.40625H2.109375s-1.40625 0 -1.40625 -1.40625v-7.03125s0 -1.40625 1.40625 -1.40625" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
||||
<path d="m16.171875 17.57625 0 -12.65625" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
||||
<path d="M11.953125 21.795a4.21875 4.21875 0 0 0 4.21875 -4.21875 4.21875 4.21875 0 0 0 4.21875 4.21875" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
||||
<path d="M11.953125 0.70125a4.21875 4.21875 0 0 1 4.21875 4.21875 4.21875 4.21875 0 0 1 4.21875 -4.21875" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
||||
</svg>
|
||||
|
||||
<!-- 타임존 -->
|
||||
<svg
|
||||
v-else-if="iconId === 'timezone'"
|
||||
class="admin-settings-nav-icon__svg pointer-events-none size-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="-0.75 -0.75 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path d="M10.546875 16.171875a5.625 5.625 0 1 0 11.25 0 5.625 5.625 0 1 0 -11.25 0Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
||||
<path d="m18.658125000000002 16.171875 -2.48625 0 0 -2.4853125" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
||||
<path d="M9.838125 21.703125a10.5478125 10.5478125 0 1 1 11.866875 -11.85375" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
||||
<path d="M8.7084375 21.4884375C7.2825 19.3959375 6.328125 15.593437499999999 6.328125 11.25S7.2825 3.105 8.7084375 1.0115625" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
||||
<path d="m0.7265625 10.546875 8.9278125 0" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
||||
<path d="M2.8115625 4.921875 19.6875 4.921875" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
||||
<path d="m1.92 16.171875 5.814375 0" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
||||
<path d="M13.7915625 1.0115625a15.9215625 15.9215625 0 0 1 2.15625 6.69" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
||||
</svg>
|
||||
|
||||
<!-- 메인 화면 -->
|
||||
<svg
|
||||
v-else-if="iconId === 'home-cover'"
|
||||
class="admin-settings-nav-icon__svg pointer-events-none size-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path d="M3 2.25h18s1.5 0 1.5 1.5v16.5s0 1.5 -1.5 1.5H3s-1.5 0 -1.5 -1.5V3.75s0 -1.5 1.5 -1.5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
||||
<path d="m1.5 6.75 21 0" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
||||
<path d="m9 6.75 0 15" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
||||
<path d="m9 14.25 13.5 0" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
||||
</svg>
|
||||
|
||||
<!-- 어나운스 바 -->
|
||||
<svg
|
||||
v-else-if="iconId === 'announcement'"
|
||||
class="admin-settings-nav-icon__svg pointer-events-none size-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="-0.75 -0.75 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path d="M6.328125 14.296875H4.21875a3.515625 3.515625 0 0 1 0 -7.03125h2.109375Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
||||
<path d="M6.328125 14.296875a20.90625 20.90625 0 0 1 11.593125 3.5100000000000002l1.0631249999999999 0.70875V3.046875l-1.0631249999999999 0.70875A20.90625 20.90625 0 0 1 6.328125 7.265625Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
||||
<path d="m21.796875 9.375 0 2.8125" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
||||
<path d="M6.328125 14.296875A6.7865625 6.7865625 0 0 0 8.4375 19.21875" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
||||
</svg>
|
||||
|
||||
<!-- Import/Export -->
|
||||
<svg
|
||||
v-else-if="iconId === 'import-export'"
|
||||
class="admin-settings-nav-icon__svg pointer-events-none size-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="-0.75 -0.75 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path d="m11.2509375 3.515625 0 11.25" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
||||
<path d="m7.0321875 10.546875 4.21875 4.21875 4.21875 -4.21875" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
||||
<path d="M21.797812500000003 14.765625v1.40625a2.8125 2.8125 0 0 1 -2.8125 2.8125h-15.46875a2.8125 2.8125 0 0 1 -2.8125 -2.8125v-1.40625" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
|
||||
</svg>
|
||||
|
||||
<!-- 스팸 필터 -->
|
||||
<svg
|
||||
v-else-if="iconId === 'spam'"
|
||||
class="admin-settings-nav-icon__svg pointer-events-none size-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path d="M19.0902 4.90918L4.9082 19.0912" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
|
||||
<span
|
||||
v-else
|
||||
class="admin-settings-nav-icon__placeholder size-4 rounded-sm border border-dashed border-[#c8ced3]"
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
<script setup>
|
||||
import {
|
||||
ANNOUNCEMENT_SNOOZE_DAYS,
|
||||
dismissAnnouncementForDays,
|
||||
dismissAnnouncementForSession,
|
||||
getAnnouncementBarTextColor,
|
||||
isAnnouncementDismissed,
|
||||
normalizeAnnouncementUrl
|
||||
} from '~/lib/announcement-bar.js'
|
||||
|
||||
const DISMISS_STORAGE_KEY = 'SITE_ANNOUNCEMENT_DISMISSED_AT'
|
||||
/** @type {number} 슬라이드 애니메이션 시간(ms) */
|
||||
const SLIDE_MS = 320
|
||||
|
||||
const { data: siteSettings } = await useFetch('/api/site-settings', {
|
||||
default: () => ({
|
||||
@@ -16,29 +21,33 @@ const { data: siteSettings } = await useFetch('/api/site-settings', {
|
||||
})
|
||||
})
|
||||
|
||||
/** DOM에 바를 둘지(애니메이션 종료 전까지 유지) */
|
||||
const inDom = ref(false)
|
||||
/** 펼침(아래로 슬라이드) 애니메이션 상태 */
|
||||
const expanded = ref(false)
|
||||
/** 숨김 처리 완료 여부 */
|
||||
const dismissed = ref(false)
|
||||
|
||||
let closeTimer = null
|
||||
|
||||
/**
|
||||
* 어나운스 바 노출 여부
|
||||
* @returns {boolean} 노출 여부
|
||||
* 설정상 어나운스 바를 켤 수 있는지
|
||||
* @returns {boolean} 가능 여부
|
||||
*/
|
||||
const isVisible = computed(() => {
|
||||
const isEligible = computed(() => {
|
||||
if (!siteSettings.value?.announcementEnabled) {
|
||||
return false
|
||||
}
|
||||
|
||||
const text = (siteSettings.value?.announcementText || '').trim()
|
||||
if (!text) {
|
||||
return false
|
||||
}
|
||||
|
||||
return !dismissed.value
|
||||
return Boolean((siteSettings.value?.announcementText || '').trim())
|
||||
})
|
||||
|
||||
const announcementText = computed(() => (siteSettings.value?.announcementText || '').trim())
|
||||
|
||||
const announcementLink = computed(() => normalizeAnnouncementUrl(siteSettings.value?.announcementUrl || ''))
|
||||
|
||||
const snoozeLabel = computed(() => `${ANNOUNCEMENT_SNOOZE_DAYS}일간 보지 않기`)
|
||||
|
||||
const barStyle = computed(() => {
|
||||
const backgroundColor = siteSettings.value?.announcementBackgroundColor || '#15171a'
|
||||
return {
|
||||
@@ -48,69 +57,140 @@ const barStyle = computed(() => {
|
||||
})
|
||||
|
||||
/**
|
||||
* 로컬에 저장된 닫기 상태를 반영한다.
|
||||
* @returns {void}
|
||||
* 어나운스 바를 펼친다.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const syncDismissedFromStorage = () => {
|
||||
if (!import.meta.client) {
|
||||
return
|
||||
}
|
||||
|
||||
const stored = localStorage.getItem(DISMISS_STORAGE_KEY)
|
||||
const updatedAt = siteSettings.value?.updatedAt || ''
|
||||
dismissed.value = Boolean(stored && stored === updatedAt)
|
||||
const openBar = async () => {
|
||||
inDom.value = true
|
||||
expanded.value = false
|
||||
await nextTick()
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
expanded.value = true
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 어나운스 바를 닫는다.
|
||||
* 어나운스 바를 접고 DOM에서 제거한다.
|
||||
* @param {() => void} persistDismiss - localStorage·sessionStorage 저장
|
||||
* @returns {void}
|
||||
*/
|
||||
const dismissAnnouncement = () => {
|
||||
dismissed.value = true
|
||||
const closeBar = (persistDismiss) => {
|
||||
if (!inDom.value) {
|
||||
return
|
||||
}
|
||||
|
||||
persistDismiss()
|
||||
expanded.value = false
|
||||
|
||||
if (closeTimer) {
|
||||
clearTimeout(closeTimer)
|
||||
}
|
||||
|
||||
closeTimer = setTimeout(() => {
|
||||
inDom.value = false
|
||||
dismissed.value = true
|
||||
closeTimer = null
|
||||
}, SLIDE_MS)
|
||||
}
|
||||
|
||||
/**
|
||||
* 이번 방문(세션) 동안만 닫는다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const dismissForSession = () => {
|
||||
closeBar(() => dismissAnnouncementForSession(siteSettings.value))
|
||||
}
|
||||
|
||||
/**
|
||||
* N일간 보지 않기로 닫는다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const dismissForSnooze = () => {
|
||||
closeBar(() => dismissAnnouncementForDays(siteSettings.value))
|
||||
}
|
||||
|
||||
watch(() => siteSettings.value?.updatedAt, async () => {
|
||||
if (!import.meta.client) {
|
||||
return
|
||||
}
|
||||
|
||||
localStorage.setItem(DISMISS_STORAGE_KEY, siteSettings.value?.updatedAt || '')
|
||||
}
|
||||
const hidden = isAnnouncementDismissed(siteSettings.value)
|
||||
dismissed.value = hidden
|
||||
|
||||
watch(() => siteSettings.value?.updatedAt, syncDismissedFromStorage)
|
||||
if (!isEligible.value || hidden) {
|
||||
expanded.value = false
|
||||
inDom.value = false
|
||||
return
|
||||
}
|
||||
|
||||
await openBar()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
syncDismissedFromStorage()
|
||||
dismissed.value = isAnnouncementDismissed(siteSettings.value)
|
||||
|
||||
if (isEligible.value && !dismissed.value) {
|
||||
openBar()
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (closeTimer) {
|
||||
clearTimeout(closeTimer)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="isVisible"
|
||||
class="site-announcement-bar relative z-30 min-h-9 w-full text-center text-sm font-medium"
|
||||
:style="barStyle"
|
||||
role="region"
|
||||
aria-label="사이트 공지"
|
||||
v-if="inDom"
|
||||
class="site-announcement-bar-shell grid transition-[grid-template-rows] duration-300 ease-out motion-reduce:transition-none"
|
||||
:class="expanded ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'"
|
||||
:style="{ transitionDuration: `${SLIDE_MS}ms` }"
|
||||
>
|
||||
<div class="site-announcement-bar__inner relative mx-auto flex max-w-[1294px] items-center justify-center px-10 py-2.5 lg:px-12">
|
||||
<component
|
||||
:is="announcementLink ? 'a' : 'span'"
|
||||
class="site-announcement-bar__text line-clamp-2"
|
||||
:class="announcementLink ? 'hover:underline' : ''"
|
||||
:href="announcementLink || undefined"
|
||||
:target="announcementLink ? '_blank' : undefined"
|
||||
:rel="announcementLink ? 'noreferrer' : undefined"
|
||||
<div class="min-h-0 overflow-hidden">
|
||||
<div
|
||||
class="site-announcement-bar relative z-30 w-full text-center text-sm font-medium"
|
||||
:style="barStyle"
|
||||
role="region"
|
||||
aria-label="사이트 공지"
|
||||
:aria-hidden="(!expanded).toString()"
|
||||
>
|
||||
{{ announcementText }}
|
||||
</component>
|
||||
<button
|
||||
class="site-announcement-bar__close absolute top-1/2 right-3 inline-flex size-7 -translate-y-1/2 items-center justify-center rounded-full opacity-80 transition hover:opacity-100 lg:right-4"
|
||||
type="button"
|
||||
aria-label="공지 닫기"
|
||||
@click="dismissAnnouncement"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M18 6 6 18" />
|
||||
<path d="m6 6 12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="site-announcement-bar__inner relative mx-auto flex min-h-9 max-w-[1294px] items-center justify-center gap-3 px-4 py-2.5 sm:px-6 lg:px-8">
|
||||
<component
|
||||
:is="announcementLink ? 'a' : 'span'"
|
||||
class="site-announcement-bar__text min-w-0 flex-1 line-clamp-2 px-6 sm:px-8"
|
||||
:class="announcementLink ? 'hover:underline' : ''"
|
||||
:href="announcementLink || undefined"
|
||||
:target="announcementLink ? '_blank' : undefined"
|
||||
:rel="announcementLink ? 'noreferrer' : undefined"
|
||||
>
|
||||
{{ announcementText }}
|
||||
</component>
|
||||
<div class="site-announcement-bar__actions absolute top-1/2 right-3 flex shrink-0 -translate-y-1/2 items-center gap-2 sm:right-4 lg:right-5">
|
||||
<button
|
||||
class="site-announcement-bar__snooze whitespace-nowrap text-xs font-medium underline-offset-2 opacity-90 transition hover:underline hover:opacity-100 sm:text-[13px]"
|
||||
type="button"
|
||||
@click="dismissForSnooze"
|
||||
>
|
||||
{{ snoozeLabel }}
|
||||
</button>
|
||||
<button
|
||||
class="site-announcement-bar__close inline-flex size-7 items-center justify-center rounded-full opacity-80 transition hover:opacity-100"
|
||||
type="button"
|
||||
aria-label="이번 방문 동안 닫기"
|
||||
@click="dismissForSession"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M18 6 6 18" />
|
||||
<path d="m6 6 12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user