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() {
-
+
{{ 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() {
-