v1.3.2: 어나운스 바 슬라이드·설정 내비 아이콘
어나운스 바는 숨김 확인 후 슬라이드 인/아웃하고 7일간 보지 않기를 지원한다. 설정 좌측 내비에 타임존·메인 화면·어나운스·Import/Export·스팸 필터 아이콘을 추가한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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