초기 로딩 스켈레톤 정리

This commit is contained in:
2026-04-07 16:29:54 +09:00
parent 76de4b940a
commit 4fbd4a2845
7 changed files with 356 additions and 5 deletions

View File

@@ -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;
}