feat. 페이지 호출시 로딩 적용

This commit is contained in:
clkim
2025-11-13 17:47:40 +09:00
parent 0d9b5dd7ad
commit 41ae618a5f
7 changed files with 189 additions and 70 deletions

View File

@@ -0,0 +1,93 @@
/* 에디터 콘텐츠 기본 스타일 유틸리티 */
/* use-base 클래스를 추가하면 하위 자식 요소들에 기본 스타일이 적용됩니다 */
@layer components {
.use-base ul,
.use-base ol {
@apply list-disc pl-6;
}
.use-base ol {
@apply list-decimal;
}
.use-base ul ul,
.use-base ol ol,
.use-base ul ol,
.use-base ol ul {
@apply mb-0;
}
.use-base table {
@apply w-full border-collapse;
}
.use-base thead {
@apply bg-gray-100;
}
.use-base th,
.use-base td {
@apply border border-gray-300 px-4 py-2 text-left;
}
.use-base th {
@apply font-semibold bg-gray-50;
}
.use-base tbody tr:nth-child(even) {
@apply bg-gray-50;
}
.use-base blockquote {
@apply border-l-4 border-gray-300 pl-4 italic text-gray-700;
}
.use-base h1 {
@apply text-2xl;
}
.use-base h2 {
@apply text-xl;
}
.use-base h3 {
@apply text-lg;
}
.use-base h4 {
@apply text-base;
}
.use-base h5 {
@apply text-sm;
}
.use-base h6 {
@apply text-xs;
}
.use-base strong,
.use-base b {
@apply font-bold;
}
.use-base em,
.use-base i {
@apply italic;
}
.use-base u {
@apply underline;
}
.use-base s {
@apply line-through;
}
.use-base a {
@apply text-blue-600 underline;
}
.use-base a:hover {
@apply text-blue-800;
}
.use-base img {
@apply max-w-full h-auto my-4;
}
.use-base pre {
@apply bg-gray-100 p-4 rounded overflow-x-auto mb-4;
}
.use-base code {
@apply bg-gray-100 px-1 py-0.5 rounded text-sm;
}
.use-base pre code {
@apply bg-transparent p-0;
}
}

View File

@@ -6,8 +6,8 @@ const { fullLoading } = storeToRefs(loadingStore)
</script>
<template>
<Transition name="fade">
<div v-if="fullLoading" class="spinner-wrap">
<Transition name="fade-out">
<div v-show="fullLoading" class="spinner-wrap">
<div class="spinner"></div>
</div>
</Transition>
@@ -15,7 +15,7 @@ const { fullLoading } = storeToRefs(loadingStore)
<style scoped>
.spinner-wrap {
@apply fixed inset-0 bg-black/90 flex items-center justify-center z-[150];
@apply fixed inset-0 bg-black pt-[96px] flex items-center justify-center sm:pt-[112px] z-[150];
}
.spinner {
@apply w-[80px] h-[80px] bg-cover bg-center bg-no-repeat bg-[url('/images/common/publisning_template_loader_black.png')];

View File

@@ -15,18 +15,21 @@ const props = defineProps<Props>()
const mainContentRef = ref<HTMLElement>()
const { locale } = useI18n()
const { getTemplateComponent } = useTemplateRegistry()
const { height: viewportH } = useWindowSize()
const { bottom: mainBottom } = useElementBounding(mainContentRef)
const { getTemplateComponent } = useTemplateRegistry()
const loadingStore = useLoadingStore()
const { isPAssApiLoading, hasApiCallStarted } = storeToRefs(loadingStore)
// 개별 메타 태그 표시 여부 확인
const shouldShowMetaTag = computed(() => props.pageData?.meta_tag_type === 2)
const pinToMain = computed(() => {
if (!mainBottom.value) return false
return mainBottom.value <= viewportH.value
})
// 개별 메타 태그 표시 여부 확인
const shouldShowMetaTag = computed(() => props.pageData?.meta_tag_type === 2)
// 템플릿 표시 여부 확인
const isTemplateVisible = (template: PageDataTemplate): boolean => {
return Boolean(
@@ -59,11 +62,6 @@ const setupSeoMeta = (metaTag: PageDataMetaTag) => {
provide('pinToMain', pinToMain)
onMounted(() => {
const { sendLog } = useAnalytics()
sendLog(locale.value, useAnalyticsLogDataDirect('view', 1))
})
// 메타 태그 설정 감시
watchEffect(() => {
if (shouldShowMetaTag.value && props.pageData?.meta_tag_json) {
@@ -71,8 +69,20 @@ watchEffect(() => {
}
})
// const loadingStore = useLoadingStore()
// loadingStore.startFullLoading()
watch(isPAssApiLoading, newVal => {
if (newVal) {
loadingStore.stopFullLoading()
}
})
onMounted(() => {
const { sendLog } = useAnalytics()
sendLog(locale.value, useAnalyticsLogDataDirect('view', 1))
if (!hasApiCallStarted.value) {
loadingStore.stopFullLoading()
}
})
</script>
<template>

View File

@@ -1,9 +1,9 @@
import { commonFetch } from '#layers/utils/apiUtil'
import { usePageDataStore } from '#layers/stores/usePageDataStore'
import { useLoadingStore } from '#layers/stores/useLoadingStore'
import { useGetGameDomain } from '#layers/composables/useGetGameDomain'
import { usePathResolver } from '#layers/composables/usePathResolver'
import type { PageDataResponse } from '#layers/types/api/pageData'
import type { GameDataValue } from '#layers/types/api/gameData'
export default defineNuxtRouteMiddleware(async (to, _from) => {
// client에서만 동작되도록 처리
@@ -13,24 +13,25 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
const stoveApiBaseUrl = runtimeConfig.public.stoveApiUrl
const apiUrl = `${stoveApiBaseUrl}/pub-comm/v2.0/template/page`
const store = usePageDataStore()
const gameDomain = useGetGameDomain()
const { getPathAfterLanguage } = usePathResolver()
const headers = useRequestHeaders()
const gameDataStore = useGameDataStore()
const gameData = gameDataStore.gameData as GameDataValue
const pageDataStore = usePageDataStore()
const loadingStore = useLoadingStore()
const gameDomain = useGetGameDomain()
const headers = useRequestHeaders()
const { getPathAfterLanguage } = usePathResolver()
const { gameData } = storeToRefs(gameDataStore)
const langCode = ssrGetFinalLocale(
to.path,
headers,
gameData?.lang_codes,
gameData?.default_lang_code
gameData.value?.lang_codes,
gameData.value?.default_lang_code
)
try {
if (to.path.includes('inspection')) {
console.log("🚀 ~ 점검페이지 접근 pageData.global")
console.log('🚀 ~ 점검페이지 접근 pageData.global')
return
}
@@ -42,10 +43,18 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
}
// pageUrl이 빈값이거나 null이면 /brand로 리다이렉트
if (!pageUrl || pageUrl === '' || pageUrl === '/' || pageUrl === `/${langCode}/`) {
if (
!pageUrl ||
pageUrl === '' ||
pageUrl === '/' ||
pageUrl === `/${langCode}/`
) {
return navigateTo(`/${langCode}/brand`, { external: false })
}
// 페이지 이동 시 로딩 시작
loadingStore.startFullLoading()
const accessToken = csrGetAccessToken()
const headers = {
@@ -54,7 +63,7 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
// 쿼리스트링에서 f 파라미터 값 추출 (CSR용)
const fValue = (to.query.f as string) || ''
// 미리보기 API 호출 처리
let finalGameDomain = gameDomain
if (fValue === 'preview') {
@@ -71,17 +80,16 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
const response = (await commonFetch('GET', apiUrl, {
headers,
query: queryParams,
loading: true,
})) as PageDataResponse | null
console.log('🚀 ~ pageData.global response:', response.value)
// 페이지 접근 권한 설정(로그인 유무)
if(response?.value?.is_login_required === 1 && !accessToken) {
if (response?.value?.is_login_required === 1 && !accessToken) {
// 로그인 레이어 팝업 띄워주기
const nuxtApp = useNuxtApp()
const modalStore = useModalStore()
const $i18n = nuxtApp.$i18n as any
const {tm} = $i18n
const { tm } = $i18n
modalStore.handleOpenConfirm({
contentText: tm('Alert_StoveLogin'),
confirmButtonText: tm('Text_StoveLogin'),
@@ -91,34 +99,37 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
})
}
if(response?.code === 91003) {
if (response?.code === 91003) {
// return navigateTo(`/${langCode}/error`, { external: false })
//클릭한 주소는 주소표시줄에 표시하도록 수정
window.history.replaceState({}, '', to.path)
// 뒤로가기 이동 시 이전 페이지로 이동되도록 수정
showError(createError({
statusCode: 404,
statusMessage: '페이지를 찾을 수 없어요.',
fatal: false, // 즉시 에러 페이지로
data: { reason: 'post-not-found' }
}))
// 뒤로가기 이동 시 이전 페이지로 이동되도록 수정
showError(
createError({
statusCode: 404,
statusMessage: '페이지를 찾을 수 없어요.',
fatal: false, // 즉시 에러 페이지로
data: { reason: 'post-not-found' },
})
)
}
if (response?.code === 0 && 'value' in response) {
store.setPageData(response.value)
pageDataStore.setPageData(response.value)
} else {
store.clearPageData()
pageDataStore.clearPageData()
}
} catch (error) {
console.error(error)
store.clearPageData()
pageDataStore.clearPageData()
showError(createError({
statusCode: error.statusCode,
statusMessage: error.message,
fatal: false, // 즉시 에러 페이지로
data: { reason: 'post-not-found' }
}))
showError(
createError({
statusCode: error.statusCode,
statusMessage: error.message,
fatal: false, // 즉시 에러 페이지로
data: { reason: 'post-not-found' },
})
)
}
})

View File

@@ -2,29 +2,42 @@ import { defineStore } from 'pinia'
export const useLoadingStore = defineStore('loadingStore', () => {
// 글로벌 로딩 표기
const fullLoading = ref(false)
const fullLoading = ref(true)
const hasApiCallStarted = ref(false)
const isPAssApiLoading = ref(false)
// 컴포넌트별 로딩 표기 - Map 대신 일반 객체 사용
const localLoadings = ref<Record<string, { active: boolean }>>({})
// 로딩 상태만 표기
const isLoading = ref(false)
/**
* 모든 로딩 상태 초기화
* 로딩 상태 초기화
*/
const initializeStore = () => {
fullLoading.value = false
localLoadings.value = {}
hasApiCallStarted.value = false
isPAssApiLoading.value = false
}
/**
* Full 로딩
*/
const startFullLoading = () => {
startApiLoading()
fullLoading.value = true
}
const startApiLoading = () => {
hasApiCallStarted.value = true
isPAssApiLoading.value = false
}
const stopFullLoading = () => {
fullLoading.value = false
if (!hasApiCallStarted.value || isPAssApiLoading.value) {
fullLoading.value = false
}
}
const finishApiLoading = () => {
setTimeout(() => {
isPAssApiLoading.value = true
}, 300)
}
/**
@@ -44,24 +57,19 @@ export const useLoadingStore = defineStore('loadingStore', () => {
return !!localLoadings.value[localId]?.active
}
/**
* 로딩 상태 변경
*/
const setLoading = (state: boolean) => {
isLoading.value = state
}
return {
fullLoading,
localLoadings,
isLoading,
hasApiCallStarted,
isPAssApiLoading,
startApiLoading,
finishApiLoading,
initializeStore,
startFullLoading,
stopFullLoading,
startLocalLoading,
stopLocalLoading,
isLocalLoading,
setLoading,
}
})

View File

@@ -68,6 +68,7 @@ const fnGetSecuritySetting = async () => {
try {
const result = await commonFetch('GET', `${apiBase}/security/setting`, {
headers,
loading: true,
})
if (result?.code === 0 && Array.isArray(result.value)) {

View File

@@ -20,12 +20,8 @@ const startLoading = (
) => {
if (!loadingStore) return
loadingStore.setLoading(true)
if (typeof loading === 'object' && loading.localId) {
loadingStore.startLocalLoading(loading.localId)
} else if (typeof loading === 'boolean') {
loadingStore.startFullLoading()
}
}
@@ -38,12 +34,12 @@ const stopLoading = (
) => {
if (!loadingStore) return
loadingStore.setLoading(false)
if (typeof loading === 'object' && loading.localId) {
loadingStore.stopLocalLoading(loading.localId)
} else if (typeof loading === 'boolean') {
loadingStore.stopFullLoading()
return
}
if (loading === true) {
loadingStore.finishApiLoading()
}
}