From 47a804c7642160bbc426c20262a3a0ad47903809 Mon Sep 17 00:00:00 2001 From: zenn Date: Mon, 6 Apr 2026 16:47:01 +0900 Subject: [PATCH] ui: add branded splash loading --- docs/history.md | 4 + docs/update.md | 5 ++ frontend/src/App.vue | 192 ++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 190 insertions(+), 11 deletions(-) diff --git a/docs/history.md b/docs/history.md index 824c1db..7a2e55a 100644 --- a/docs/history.md +++ b/docs/history.md @@ -1,5 +1,9 @@ # 의사결정 이력 +## 2026-04-06 v1.1.0 +- `v1.0.105`까지 공개 기준 1.0 라인에서 초기 안정화 작업을 이어왔고, 이번부터는 사용자 경험 polish 성격의 개선을 `v1.1.x` 라인으로 올려 진행하기로 정리했다. +- 최초 접속 로딩은 인증 상태를 설명하는 문구보다, 사용자가 사이트에 들어왔다는 느낌을 주는 짧은 브랜드 스플래시가 더 자연스럽다. 다만 실제 서버 연결 실패나 점검 상황은 여전히 명확한 안내가 필요하므로, 백엔드 fallback 화면은 기존 기능 안내형 UI를 유지한다. + ## 2026-04-06 v1.0.105 - 인증 상태와 템플릿 데이터는 모두 비동기 로딩이라, 응답 전 빈 상태를 먼저 보여주면 “로그아웃된 것처럼 보임” 또는 “템플릿이 없는 사이트처럼 보임”으로 오해될 수 있다. 따라서 앱 셸에서는 인증 확인이 끝날 때까지 공통 로딩을 보여주고, 홈 화면은 템플릿 API 응답 전 빈 상태 대신 로딩 카드를 보여주는 편이 더 자연스럽다고 정리했다. diff --git a/docs/update.md b/docs/update.md index 632df3c..7879cc7 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,10 @@ # 업데이트 로그 +## 2026-04-06 v1.1.0 +- 이번 작업부터 공개 후 개선 흐름을 `v1.1.x` 라인으로 올려 이어간다. 이전 최신 문서 기준은 `v1.0.105`다. +- 사이트 최초 접속 로딩 화면을 `접속 정보를 확인하고 있어요.` 같은 기능 안내 중심 문구에서, Tier Maker 브랜드 마크와 진행 애니메이션이 있는 스플래시 스타일로 변경했다. +- 확인: `npm run build` + ## 2026-04-06 v1.0.105 - 사이트 최초 접속 시 로그인/세션 확인이 끝나기 전 게스트 UI처럼 보였다가 뒤늦게 로그인 상태로 바뀌는 깜빡임을 줄이기 위해, 앱 공통 초기 로딩 화면을 추가했다. - 홈 화면의 주제 템플릿 목록도 API 응답 전에는 `표시할 주제 템플릿이 없어요.`를 바로 보여주지 않고, `주제 템플릿을 불러오고 있어요.` 로딩 카드가 먼저 나오도록 정리했다. diff --git a/frontend/src/App.vue b/frontend/src/App.vue index e14bc84..8776778 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -584,10 +584,18 @@ function reloadApp() { @@ -860,6 +868,15 @@ function reloadApp() { var(--theme-shell-bg); } +.appLoading { + overflow: hidden; + background: + radial-gradient(circle at 50% 18%, rgba(127, 231, 214, 0.22), transparent 28%), + radial-gradient(circle at 26% 76%, rgba(95, 202, 255, 0.18), transparent 30%), + radial-gradient(circle at 78% 68%, rgba(167, 139, 250, 0.14), transparent 28%), + var(--theme-shell-bg); +} + .backendFallback__card, .appLoading__card { width: min(100%, 560px); @@ -872,6 +889,102 @@ function reloadApp() { box-shadow: inset 0 1px 0 var(--theme-card-shadow); } +.appLoading__card { + position: relative; + width: min(100%, 420px); + justify-items: center; + gap: 14px; + padding: 42px 32px; + overflow: hidden; + text-align: center; + background: + linear-gradient(145deg, color-mix(in srgb, var(--theme-card-bg) 92%, white), var(--theme-card-bg)), + var(--theme-card-bg); +} + +.appLoading__card::before { + content: ''; + position: absolute; + inset: -42%; + background: conic-gradient(from 90deg, transparent, rgba(127, 231, 214, 0.18), transparent 34%, rgba(95, 202, 255, 0.16), transparent 68%); + animation: appLoadingAura 4.8s linear infinite; +} + +.appLoading__card > * { + position: relative; + z-index: 1; +} + +.appLoading__brand { + position: relative; + width: 112px; + height: 112px; + display: grid; + place-items: center; + margin-bottom: 6px; +} + +.appLoading__logo { + position: relative; + width: 76px; + height: 76px; + display: grid; + place-items: center; + border-radius: 24px; + border: 1px solid color-mix(in srgb, var(--theme-accent-strong) 34%, transparent); + background: + linear-gradient(135deg, rgba(127, 231, 214, 0.2), rgba(95, 202, 255, 0.12)), + color-mix(in srgb, var(--theme-card-bg) 86%, #090d16); + box-shadow: + 0 20px 60px rgba(0, 0, 0, 0.28), + 0 0 34px rgba(127, 231, 214, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.12); + animation: appLoadingFloat 2.6s ease-in-out infinite; +} + +.appLoading__logoMain { + color: var(--theme-accent-strong); + font-size: 44px; + font-weight: 950; + line-height: 1; + letter-spacing: -0.12em; +} + +.appLoading__logoBar { + position: absolute; + right: 18px; + bottom: 16px; + width: 10px; + height: 24px; + border-radius: 999px; + background: #5fcaff; + box-shadow: 0 0 18px rgba(95, 202, 255, 0.54); +} + +.appLoading__orb { + position: absolute; + width: 14px; + height: 14px; + border-radius: 999px; + filter: drop-shadow(0 0 12px currentColor); + animation: appLoadingOrbit 2.4s ease-in-out infinite; +} + +.appLoading__orb--mint { + top: 10px; + right: 18px; + color: #7fe7d6; + background: currentColor; +} + +.appLoading__orb--blue { + left: 14px; + bottom: 18px; + color: #5fcaff; + background: currentColor; + animation-delay: -1.1s; +} + .backendFallback__eyebrow, .appLoading__eyebrow { color: var(--theme-accent-strong); @@ -889,6 +1002,10 @@ function reloadApp() { letter-spacing: -0.04em; } +.appLoading__title { + font-size: clamp(27px, 4vw, 38px); +} + .backendFallback__desc, .appLoading__desc { margin: 0; @@ -897,21 +1014,74 @@ function reloadApp() { line-height: 1.7; } -.appLoading__spinner { - width: 36px; - height: 36px; +.appLoading__progress { + width: min(100%, 220px); + height: 6px; + margin-top: 12px; + overflow: hidden; border-radius: 999px; - border: 3px solid color-mix(in srgb, var(--theme-text-faint) 34%, transparent); - border-top-color: var(--theme-accent-strong); - animation: appLoadingSpin 820ms linear infinite; + background: color-mix(in srgb, var(--theme-text-faint) 18%, transparent); } -@keyframes appLoadingSpin { +.appLoading__progress span { + display: block; + width: 42%; + height: 100%; + border-radius: inherit; + background: linear-gradient(90deg, #7fe7d6, #5fcaff); + box-shadow: 0 0 20px rgba(127, 231, 214, 0.46); + animation: appLoadingProgress 1.35s ease-in-out infinite; +} + +@keyframes appLoadingAura { to { transform: rotate(360deg); } } +@keyframes appLoadingFloat { + 0%, + 100% { + transform: translateY(0) scale(1); + } + + 50% { + transform: translateY(-6px) scale(1.02); + } +} + +@keyframes appLoadingOrbit { + 0%, + 100% { + transform: translate(0, 0); + opacity: 0.75; + } + + 50% { + transform: translate(8px, -10px); + opacity: 1; + } +} + +@keyframes appLoadingProgress { + 0% { + transform: translateX(-110%); + } + + 100% { + transform: translateX(250%); + } +} + +@media (prefers-reduced-motion: reduce) { + .appLoading__card::before, + .appLoading__logo, + .appLoading__orb, + .appLoading__progress span { + animation: none; + } +} + .backendFallback__actions { display: flex; justify-content: flex-start;