diff --git a/docs/update.md b/docs/update.md index 86247a8..de83801 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,9 @@ # 업데이트 로그 +## 2026-04-01 v1.3.29 +- 책 아이콘 사용법 모달 진입점은 항상 보이는 오른쪽 사이드 하단 버튼 대신, Settings 화면에서만 왼쪽 사이드 하단의 보조 액션 버튼으로 옮겨 더 필요할 때만 찾게 되는 문맥형 진입 방식으로 정리함. +- 인증 스토어에 초기 세션 동기화 완료 상태를 추가하고, 앱 셸·로그인 화면·프로필 화면은 세션 확인 전까지 비로그인 UI를 먼저 그리지 않도록 보강해 첫 진입 시 화면이 갑자기 로그인 상태로 뒤집히는 플래시를 줄임. + ## 2026-04-01 v1.3.28 - 책 아이콘 기반 사용법 모달은 기존의 단순 제작 흐름 안내를 넘어, 다른 사람 티어표 복사, 템플릿 업그레이드 요청, 새 템플릿 추가 요청, 즐겨찾기/내 티어표 관리까지 포함한 전체 기능 안내 허브로 확장함. - 사용법 모달 제목과 단계 표기를 더 넓은 개념의 `기능 안내` 기준으로 정리하고, 실제 스크린샷이 없어도 설명만으로 핵심 기능을 순서대로 이해할 수 있게 단계 문구를 전면 보강함. diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 0904013..1eb87b0 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -31,7 +31,8 @@ const viewportWidth = ref(typeof window !== 'undefined' ? window.innerWidth : 14 provide('rightRailOpen', rightRailOpen) provide('localRightRailTarget', '#local-right-rail-root') -const isAdmin = computed(() => !!auth.user?.isAdmin) +const authReady = computed(() => auth.hydrated) +const isAdmin = computed(() => authReady.value && !!auth.user?.isAdmin) const isPreviewMode = computed(() => route.query.preview === '1') const usesLocalRightRail = computed(() => ['editEditor', 'newEditor', 'admin'].includes(String(route.name || ''))) const isRightRailOverlay = computed(() => viewportWidth.value <= 1200) @@ -44,7 +45,10 @@ const accountName = computed(() => { if (email) return email.split('@')[0] || email return 'Guest' }) -const accountEmail = computed(() => (auth.user?.email || '').trim() || '로그인 후 개인 메뉴를 사용할 수 있어요.') +const accountEmail = computed(() => { + if (!authReady.value) return '계정 상태를 확인하고 있어요.' + return (auth.user?.email || '').trim() || '로그인 후 개인 메뉴를 사용할 수 있어요.' +}) const shellStyle = computed(() => ({ '--left-rail-width': leftRailCollapsed.value ? '76px' : '248px', '--right-rail-width': !isRightRailOverlay.value && rightRailOpen.value ? '325px' : '0px', @@ -56,9 +60,10 @@ const leftNavItems = computed(() => { { key: 'favorites', label: 'Favorites', path: '/favorites', iconSrc: iconFavorite, requiresAuth: true }, { key: 'profile', label: 'Settings', path: '/profile', iconSrc: iconSettings, requiresAuth: true }, ] - return items.filter((item) => !item.requiresAuth || auth.user) + return items.filter((item) => !item.requiresAuth || (authReady.value && auth.user)) }) const showRightRailAction = computed(() => false) +const showSettingsGuideButton = computed(() => route.name === 'profile') const guideSteps = [ { id: 'select-game', @@ -123,6 +128,7 @@ const isGuideNextDisabled = computed(() => guideStepIndex.value >= guideSteps.le const showGameHubViewToggle = computed(() => route.name === 'gameHub') const gameHubViewMode = computed(() => (route.query.view === 'list' ? 'list' : 'grid')) const leftBottomPrimaryAction = computed(() => { + if (!authReady.value) return null if (route.name === 'home' && auth.user) { return { label: '커스텀 티어표 만들기', to: '/editor/freeform/new' } } @@ -401,7 +407,7 @@ function submitGlobalSearch() {
-
+
avatar
{{ accountName[0]?.toUpperCase() || 'U' }}
@@ -442,8 +448,12 @@ function submitGlobalSearch() {
{{ leftBottomPrimaryAction.label }} - 관리자 메뉴 - 로그인 + + 관리자 메뉴 + 로그인
@@ -567,9 +577,6 @@ function submitGlobalSearch() { -
@@ -960,6 +967,7 @@ function submitGlobalSearch() { display: inline-flex; justify-content: center; align-items: center; + gap: 8px; padding: 12px 14px; border-radius: 14px; border: 1px solid rgba(255, 255, 255, 0.12); @@ -970,6 +978,14 @@ function submitGlobalSearch() { font-weight: 800; } +.adminButton--icon { + text-align: center; +} + +.adminButton__icon { + flex: 0 0 auto; +} + .appMain { min-width: 0; min-height: 0; @@ -1084,25 +1100,6 @@ function submitGlobalSearch() { padding-top: 12px; } -.guideDockButton { - width: 42px; - height: 42px; - border-radius: 14px; - border: 1px solid rgba(255, 255, 255, 0.08); - background: rgba(255, 255, 255, 0.04); - color: rgba(255, 255, 255, 0.78); - display: inline-flex; - align-items: center; - justify-content: center; - cursor: pointer; - flex: 0 0 auto; -} - -.guideDockButton:hover { - background: rgba(255, 255, 255, 0.08); - color: rgba(255, 255, 255, 0.96); -} - .rightRailAction__button { width: 100%; padding: 12px 14px; diff --git a/frontend/src/stores/auth.js b/frontend/src/stores/auth.js index 59c330c..27ebf26 100644 --- a/frontend/src/stores/auth.js +++ b/frontend/src/stores/auth.js @@ -5,30 +5,40 @@ export const useAuthStore = defineStore('auth', { state: () => ({ user: null, status: 'idle', + hydrated: false, }), actions: { async refresh() { + if (this.status === 'loading') return this.user this.status = 'loading' try { const data = await api.me() this.user = data.user + return this.user + } catch (error) { + this.user = null + return null } finally { this.status = 'idle' + this.hydrated = true } }, async signup(email, password) { const user = await api.signup({ email, password }) this.user = user + this.hydrated = true return user }, async login(email, password) { const user = await api.login({ email, password }) this.user = user + this.hydrated = true return user }, async logout() { await api.logout() this.user = null + this.hydrated = true }, }, }) diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue index 991da1a..cf9abd3 100644 --- a/frontend/src/views/LoginView.vue +++ b/frontend/src/views/LoginView.vue @@ -30,8 +30,15 @@ const description = computed(() => : '저장한 티어표와 즐겨찾기, 프로필 설정을 이어서 관리할 수 있어요.' ) const submitLabel = computed(() => (mode.value === 'signup' ? '가입하기' : '로그인')) +const authReady = computed(() => auth.hydrated) +const checkingSession = computed(() => !authReady.value || auth.status === 'loading') onMounted(async () => { + if (!auth.hydrated) await auth.refresh() + if (auth.user) { + router.replace(typeof route.query.redirect === 'string' ? route.query.redirect : '/me') + return + } try { const meta = await api.authMeta() hasUsers.value = !!meta.hasUsers @@ -40,6 +47,15 @@ onMounted(async () => { } }) +watch( + () => [auth.hydrated, auth.user], + ([hydrated, user]) => { + if (!hydrated || !user) return + router.replace(typeof route.query.redirect === 'string' ? route.query.redirect : '/me') + }, + { immediate: true } +) + async function submit() { error.value = '' if (mode.value === 'signup' && password.value !== passwordConfirm.value) { @@ -66,7 +82,11 @@ async function submit() {
-
+
+
로그인 상태를 확인하고 있어요.
+
+ +
-
+
+
계정 정보를 불러오고 있어요.
+
+ +