릴리스: v1.3.29 가이드 진입점과 인증 초기화 안정화
This commit is contained in:
@@ -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() {
|
||||
|
||||
<div class="leftRail__body">
|
||||
<div class="leftRail__content">
|
||||
<div v-if="auth.user" class="appUserCard">
|
||||
<div v-if="authReady && auth.user" class="appUserCard">
|
||||
<div class="appUserCard__button">
|
||||
<img v-if="avatarUrl" :src="avatarUrl" class="appUserCard__avatar" alt="avatar" />
|
||||
<div v-else class="appUserCard__avatar appUserCard__avatar--fallback">{{ accountName[0]?.toUpperCase() || 'U' }}</div>
|
||||
@@ -442,8 +448,12 @@ function submitGlobalSearch() {
|
||||
</div>
|
||||
<div class="leftRail__bottom">
|
||||
<RouterLink v-if="leftBottomPrimaryAction" :to="leftBottomPrimaryAction.to" class="adminButton">{{ leftBottomPrimaryAction.label }}</RouterLink>
|
||||
<RouterLink v-if="isAdmin" to="/admin" class="adminButton">관리자 메뉴</RouterLink>
|
||||
<RouterLink v-else-if="!auth.user" to="/login" class="adminButton">로그인</RouterLink>
|
||||
<button v-if="showSettingsGuideButton" class="adminButton adminButton--icon" type="button" @click="openGuideModal()">
|
||||
<SvgIcon :src="iconMenuBook" :size="18" class="adminButton__icon" />
|
||||
<span>가이드 보기</span>
|
||||
</button>
|
||||
<RouterLink v-if="authReady && isAdmin" to="/admin" class="adminButton">관리자 메뉴</RouterLink>
|
||||
<RouterLink v-else-if="authReady && !auth.user" to="/login" class="adminButton">로그인</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -567,9 +577,6 @@ function submitGlobalSearch() {
|
||||
</button>
|
||||
</section>
|
||||
</template>
|
||||
<button class="guideDockButton" type="button" aria-label="사용법 열기" @click="openGuideModal()">
|
||||
<SvgIcon :src="iconMenuBook" :size="22" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="authScreen">
|
||||
<section v-if="checkingSession" class="authScreen authScreen--loading">
|
||||
<div class="authLoading">로그인 상태를 확인하고 있어요.</div>
|
||||
</section>
|
||||
|
||||
<section v-else class="authScreen">
|
||||
<div class="authTabs" role="tablist" aria-label="로그인 또는 회원가입">
|
||||
<button type="button" class="authTabs__button" :class="{ 'authTabs__button--active': mode === 'login' }" @click="mode = 'login'">
|
||||
로그인
|
||||
@@ -128,6 +148,16 @@ async function submit() {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.authScreen--loading {
|
||||
min-height: 220px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.authLoading {
|
||||
color: rgba(255, 255, 255, 0.62);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.authTabs {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
|
||||
@@ -30,14 +30,19 @@ const avatarUrl = computed(() => {
|
||||
return toApiUrl(auth.user.avatarSrc)
|
||||
})
|
||||
|
||||
const authReady = computed(() => auth.hydrated)
|
||||
|
||||
const displayInitial = computed(() => {
|
||||
const email = auth.user?.email || 'U'
|
||||
return email[0].toUpperCase()
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await auth.refresh()
|
||||
if (!auth.user) router.push('/login')
|
||||
if (!auth.hydrated) await auth.refresh()
|
||||
if (!auth.user) {
|
||||
router.replace('/login')
|
||||
return
|
||||
}
|
||||
nickname.value = auth.user?.nickname || ''
|
||||
removeAvatar.value = false
|
||||
})
|
||||
@@ -121,7 +126,11 @@ async function logout() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section v-if="auth.user" class="settingsScreen">
|
||||
<section v-if="!authReady" class="settingsScreen settingsScreen--loading">
|
||||
<div class="settingsLoading">계정 정보를 불러오고 있어요.</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="auth.user" class="settingsScreen">
|
||||
<div class="settingsIdentity">
|
||||
<div class="avatarButtonWrap">
|
||||
<button class="avatarButton" type="button" @click="openAvatarPicker">
|
||||
@@ -185,6 +194,16 @@ async function logout() {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.settingsScreen--loading {
|
||||
min-height: 240px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.settingsLoading {
|
||||
color: rgba(255, 255, 255, 0.62);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.settingsIdentity {
|
||||
display: grid;
|
||||
grid-template-columns: 120px minmax(0, 1fr);
|
||||
|
||||
Reference in New Issue
Block a user