초기 로딩 스켈레톤 정리
This commit is contained in:
@@ -51,6 +51,7 @@ const backendState = ref('online')
|
||||
const backendMessage = ref('')
|
||||
const isFullscreenActive = ref(false)
|
||||
const unreadCommentCount = ref(0)
|
||||
const isBootReady = ref(false)
|
||||
provide('rightRailOpen', rightRailOpen)
|
||||
provide('localRightRailTarget', '#local-right-rail-root')
|
||||
|
||||
@@ -378,7 +379,11 @@ onMounted(async () => {
|
||||
if (savedTheme === 'light' || savedTheme === 'dark') applyTheme(savedTheme)
|
||||
else applyTheme('dark')
|
||||
}
|
||||
await auth.refresh()
|
||||
await Promise.all([
|
||||
auth.refresh(),
|
||||
new Promise((resolve) => setTimeout(resolve, 140)),
|
||||
])
|
||||
isBootReady.value = true
|
||||
if (typeof window !== 'undefined') {
|
||||
syncViewportWidth()
|
||||
syncFullscreenState()
|
||||
@@ -691,6 +696,24 @@ function reloadApp() {
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
<template v-else-if="!isBootReady">
|
||||
<main class="bootGate">
|
||||
<section class="bootGate__shell">
|
||||
<div class="bootGate__brand">
|
||||
<div class="bootGate__logo"></div>
|
||||
<div class="bootGate__copy">
|
||||
<div class="bootGate__title">Tier Maker</div>
|
||||
<div class="bootGate__desc">계정 상태와 화면 구성을 준비하고 있어요.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bootGate__panels">
|
||||
<div class="bootGate__panel bootGate__panel--nav"></div>
|
||||
<div class="bootGate__panel bootGate__panel--main"></div>
|
||||
<div class="bootGate__panel bootGate__panel--side"></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button v-if="isMobileLayout && mobileLeftNavOpen" class="leftRailBackdrop" type="button" aria-label="왼쪽 패널 닫기" @click="toggleLeftRail"></button>
|
||||
|
||||
@@ -1004,6 +1027,95 @@ function reloadApp() {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.bootGate {
|
||||
min-width: 100dvw;
|
||||
min-height: 100dvh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
background: var(--theme-shell-bg);
|
||||
}
|
||||
|
||||
.bootGate__shell {
|
||||
width: min(100%, 1180px);
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.bootGate__brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 18px 20px;
|
||||
border-radius: 24px;
|
||||
border: 1px solid var(--theme-border);
|
||||
background: var(--theme-rail-bg);
|
||||
}
|
||||
|
||||
.bootGate__logo,
|
||||
.bootGate__panel {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bootGate__logo::after,
|
||||
.bootGate__panel::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
transform: translateX(-100%);
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.08), transparent);
|
||||
animation: bootGateShimmer 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.bootGate__logo {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 18px;
|
||||
border: 1px solid var(--theme-border);
|
||||
background: var(--theme-surface-soft);
|
||||
}
|
||||
|
||||
.bootGate__copy {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.bootGate__title {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.03em;
|
||||
color: var(--theme-text-strong);
|
||||
}
|
||||
|
||||
.bootGate__desc {
|
||||
color: var(--theme-text-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.bootGate__panels {
|
||||
display: grid;
|
||||
grid-template-columns: 248px minmax(0, 1fr) 320px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.bootGate__panel {
|
||||
border-radius: 24px;
|
||||
border: 1px solid var(--theme-border);
|
||||
background: var(--theme-rail-bg);
|
||||
min-height: 72dvh;
|
||||
}
|
||||
|
||||
.bootGate__panel--main {
|
||||
background: var(--theme-shell-bg);
|
||||
}
|
||||
|
||||
@keyframes bootGateShimmer {
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.leftRail,
|
||||
.rightRail {
|
||||
min-height: 100dvh;
|
||||
@@ -2179,6 +2291,32 @@ function reloadApp() {
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.bootGate {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.bootGate__shell {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.bootGate__brand {
|
||||
padding: 16px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.bootGate__panels {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.bootGate__panel--nav,
|
||||
.bootGate__panel--side {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bootGate__panel--main {
|
||||
min-height: 72dvh;
|
||||
}
|
||||
|
||||
.guideModal {
|
||||
padding: 20px 12px;
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ const itemContextMenu = ref({
|
||||
y: 0,
|
||||
itemId: '',
|
||||
})
|
||||
const isEditorLoading = ref(true)
|
||||
let editorLoadToken = 0
|
||||
|
||||
const boardEl = ref(null)
|
||||
@@ -1302,6 +1303,7 @@ function resetEditorStateForRoute() {
|
||||
selectedItemId.value = ''
|
||||
recentDragFinishedAt.value = 0
|
||||
savedEditorSnapshot.value = ''
|
||||
isEditorLoading.value = true
|
||||
closeItemContextMenu()
|
||||
resetTemplateRequestDrafts()
|
||||
}
|
||||
@@ -1309,7 +1311,9 @@ function resetEditorStateForRoute() {
|
||||
async function loadEditorState() {
|
||||
const loadToken = ++editorLoadToken
|
||||
resetEditorStateForRoute()
|
||||
await auth.refresh()
|
||||
if (!auth.hydrated) {
|
||||
await auth.refresh()
|
||||
}
|
||||
if (loadToken !== editorLoadToken) return
|
||||
|
||||
authorName.value = (auth.user?.nickname || '').trim()
|
||||
@@ -1391,6 +1395,7 @@ async function loadEditorState() {
|
||||
} catch (e) {
|
||||
if (loadToken !== editorLoadToken) return
|
||||
error.value = '티어표를 불러오지 못했어요.'
|
||||
isEditorLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1402,6 +1407,8 @@ async function loadEditorState() {
|
||||
if (canEdit.value) {
|
||||
await initSortables()
|
||||
}
|
||||
if (loadToken !== editorLoadToken) return
|
||||
isEditorLoading.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
@@ -1444,7 +1451,27 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-if="previewMode" class="previewOnly" :style="{ '--thumb-size': `${iconSize}px` }">
|
||||
<section v-if="previewMode && isEditorLoading" class="editorSkeleton editorSkeleton--preview">
|
||||
<header class="pageHead">
|
||||
<div class="pageHead__main">
|
||||
<div class="editorSkeleton__line editorSkeleton__line--eyebrow"></div>
|
||||
<div class="editorSkeleton__line editorSkeleton__line--title"></div>
|
||||
<div class="editorSkeleton__line editorSkeleton__line--desc"></div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="editorSkeleton__previewCard">
|
||||
<div class="editorSkeleton__board">
|
||||
<div class="editorSkeleton__row" v-for="index in 4" :key="`preview-row-${index}`">
|
||||
<div class="editorSkeleton__label"></div>
|
||||
<div class="editorSkeleton__cells">
|
||||
<div class="editorSkeleton__cell" v-for="cellIndex in 3" :key="`preview-cell-${index}-${cellIndex}`"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="previewMode" class="previewOnly" :style="{ '--thumb-size': `${iconSize}px` }">
|
||||
<header class="pageHead">
|
||||
<div class="pageHead__main">
|
||||
<div class="pageHead__eyebrow">Preview</div>
|
||||
@@ -1521,6 +1548,29 @@ onUnmounted(() => {
|
||||
</Teleport>
|
||||
</section>
|
||||
|
||||
<template v-else-if="isEditorLoading">
|
||||
<section class="editorSkeleton" :style="{ '--thumb-size': `${iconSize}px` }">
|
||||
<div class="editorSkeleton__head">
|
||||
<div class="editorSkeleton__line editorSkeleton__line--title"></div>
|
||||
<div class="editorSkeleton__line editorSkeleton__line--desc"></div>
|
||||
</div>
|
||||
<div class="editorSkeleton__body">
|
||||
<div class="editorSkeleton__board">
|
||||
<div class="editorSkeleton__toolbar">
|
||||
<div class="editorSkeleton__chip" v-for="index in 3" :key="`toolbar-${index}`"></div>
|
||||
</div>
|
||||
<div class="editorSkeleton__row" v-for="index in 4" :key="`editor-row-${index}`">
|
||||
<div class="editorSkeleton__label"></div>
|
||||
<div class="editorSkeleton__cells">
|
||||
<div class="editorSkeleton__cell" v-for="cellIndex in 3" :key="`editor-cell-${index}-${cellIndex}`"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editorSkeleton__side"></div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
|
||||
<div v-if="isSaveModalOpen" class="modalOverlay" @click.self="closeSaveModal">
|
||||
@@ -2023,6 +2073,127 @@ onUnmounted(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.editorSkeleton {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
.editorSkeleton__head {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
.editorSkeleton__body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, clamp(680px, 58vw, 960px)) minmax(280px, 1fr);
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
.editorSkeleton__previewCard,
|
||||
.editorSkeleton__board,
|
||||
.editorSkeleton__side,
|
||||
.editorSkeleton__line,
|
||||
.editorSkeleton__label,
|
||||
.editorSkeleton__cell,
|
||||
.editorSkeleton__chip {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.editorSkeleton__previewCard,
|
||||
.editorSkeleton__board,
|
||||
.editorSkeleton__side {
|
||||
border-radius: 22px;
|
||||
border: 1px solid var(--theme-border);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
color-mix(in srgb, var(--theme-card-bg) 96%, transparent),
|
||||
color-mix(in srgb, var(--theme-card-bg-hover) 92%, transparent)
|
||||
);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
.editorSkeleton__previewCard,
|
||||
.editorSkeleton__board {
|
||||
padding: 20px;
|
||||
}
|
||||
.editorSkeleton__side {
|
||||
min-height: 540px;
|
||||
}
|
||||
.editorSkeleton__previewCard::after,
|
||||
.editorSkeleton__board::after,
|
||||
.editorSkeleton__side::after,
|
||||
.editorSkeleton__line::after,
|
||||
.editorSkeleton__label::after,
|
||||
.editorSkeleton__cell::after,
|
||||
.editorSkeleton__chip::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.08) 48%, transparent 100%);
|
||||
transform: translateX(-100%);
|
||||
animation: editorSkeletonShimmer 1.4s ease-in-out infinite;
|
||||
}
|
||||
.editorSkeleton__line {
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--theme-surface-soft-2) 88%, transparent);
|
||||
}
|
||||
.editorSkeleton__line--eyebrow {
|
||||
width: 92px;
|
||||
height: 12px;
|
||||
}
|
||||
.editorSkeleton__line--title {
|
||||
width: min(420px, 78%);
|
||||
height: 34px;
|
||||
}
|
||||
.editorSkeleton__line--desc {
|
||||
width: min(560px, 92%);
|
||||
height: 14px;
|
||||
}
|
||||
.editorSkeleton__toolbar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.editorSkeleton__chip {
|
||||
width: 88px;
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--theme-surface-soft-2) 84%, transparent);
|
||||
}
|
||||
.editorSkeleton__row {
|
||||
display: grid;
|
||||
grid-template-columns: 132px 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
.editorSkeleton__row + .editorSkeleton__row {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.editorSkeleton__label {
|
||||
min-height: 110px;
|
||||
border-radius: 14px;
|
||||
background: color-mix(in srgb, var(--theme-surface-soft-2) 88%, transparent);
|
||||
}
|
||||
.editorSkeleton__cells {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.editorSkeleton__cell {
|
||||
min-height: 110px;
|
||||
border-radius: 14px;
|
||||
background: color-mix(in srgb, var(--theme-surface-soft) 74%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--theme-border) 88%, transparent);
|
||||
}
|
||||
.editorSkeleton--preview .editorSkeleton__board {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@keyframes editorSkeletonShimmer {
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.head {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
@@ -3309,6 +3480,21 @@ onUnmounted(() => {
|
||||
border-radius: 14px;
|
||||
}
|
||||
@media (max-width: 980px) {
|
||||
.editorSkeleton__body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.editorSkeleton__side {
|
||||
min-height: 320px;
|
||||
}
|
||||
.editorSkeleton__row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.editorSkeleton__label {
|
||||
min-height: 44px;
|
||||
}
|
||||
.editorSkeleton__cells {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.previewOnly__row,
|
||||
.row {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -3389,6 +3575,15 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.editorSkeleton__previewCard,
|
||||
.editorSkeleton__board,
|
||||
.editorSkeleton__side {
|
||||
border-radius: 18px;
|
||||
}
|
||||
.editorSkeleton__previewCard,
|
||||
.editorSkeleton__board {
|
||||
padding: 16px;
|
||||
}
|
||||
.previewOnly {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user