Clarify auth states
This commit is contained in:
@@ -86,3 +86,5 @@
|
|||||||
- 내역 헤더의 검색과 필터 버튼을 `assets/icons/search.svg`, `assets/icons/tune.svg` 인라인 SVG로 교체했다.
|
- 내역 헤더의 검색과 필터 버튼을 `assets/icons/search.svg`, `assets/icons/tune.svg` 인라인 SVG로 교체했다.
|
||||||
- CSV 내보내기 컬럼을 한글 컬럼명과 앱의 거래 흐름에 맞는 순서로 정리하고 Excel 호환 BOM을 추가했다.
|
- CSV 내보내기 컬럼을 한글 컬럼명과 앱의 거래 흐름에 맞는 순서로 정리하고 Excel 호환 BOM을 추가했다.
|
||||||
- 빈 데이터 상태 화면을 공통 컴포넌트로 분리하고 내역, 통계, 자산, 설정, 공유 화면에 적용했다.
|
- 빈 데이터 상태 화면을 공통 컴포넌트로 분리하고 내역, 통계, 자산, 설정, 공유 화면에 적용했다.
|
||||||
|
- 로그인 화면에 세션 만료/로그인 필요 안내를 추가하고, 로그인 후 원래 접근하려던 화면으로 돌아가도록 했다.
|
||||||
|
- 로그인/회원가입 입력값을 요청 전에 먼저 검증하고 네트워크 오류 메시지를 더 명확하게 표시하도록 했다.
|
||||||
|
|||||||
@@ -56,3 +56,4 @@ user.email: zenn.message@gmail.com
|
|||||||
|
|
||||||
- 의미 있는 수정 단위마다 변경 내역을 문서에 남기고 커밋/푸시한다.
|
- 의미 있는 수정 단위마다 변경 내역을 문서에 남기고 커밋/푸시한다.
|
||||||
- 원격 저장소는 `https://git.sori.studio/zenn/money.sori.studio.git`를 사용한다.
|
- 원격 저장소는 `https://git.sori.studio/zenn/money.sori.studio.git`를 사용한다.
|
||||||
|
- 인증이 필요한 화면에서 로그인 화면으로 이동할 때는 로그인 필요/세션 만료처럼 사용자가 이유를 이해할 수 있는 안내를 함께 제공한다.
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
|||||||
if (to.path === '/login' || to.path.startsWith('/share/')) return
|
if (to.path === '/login' || to.path.startsWith('/share/')) return
|
||||||
|
|
||||||
const session = useState<{ id: string; email: string; name: string } | null>('session')
|
const session = useState<{ id: string; email: string; name: string } | null>('session')
|
||||||
|
const sessionCookie = useCookie<string | null>('money_session')
|
||||||
|
const authReason = sessionCookie.value ? 'expired' : 'required'
|
||||||
|
|
||||||
if (session.value === undefined) {
|
if (session.value === undefined) {
|
||||||
try {
|
try {
|
||||||
@@ -12,6 +14,12 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!session.value) {
|
if (!session.value) {
|
||||||
return navigateTo('/login')
|
return navigateTo({
|
||||||
|
path: '/login',
|
||||||
|
query: {
|
||||||
|
reason: authReason,
|
||||||
|
redirect: to.fullPath === '/' ? undefined : to.fullPath
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -46,7 +46,12 @@
|
|||||||
required
|
required
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="error" class="text-sm font-medium text-red-600">{{ error }}</p>
|
<p v-if="authNotice && !error" class="rounded-md bg-brand-50 px-3 py-2 text-sm font-semibold text-brand-700">
|
||||||
|
{{ authNotice }}
|
||||||
|
</p>
|
||||||
|
<p v-if="error" class="rounded-md bg-red-50 px-3 py-2 text-sm font-semibold text-red-600">
|
||||||
|
{{ error }}
|
||||||
|
</p>
|
||||||
<button class="btn-primary w-full" type="submit" :disabled="pending">
|
<button class="btn-primary w-full" type="submit" :disabled="pending">
|
||||||
{{ pending ? '확인 중...' : mode === 'login' ? '로그인' : '가입하고 시작하기' }}
|
{{ pending ? '확인 중...' : mode === 'login' ? '로그인' : '가입하고 시작하기' }}
|
||||||
</button>
|
</button>
|
||||||
@@ -74,18 +79,35 @@ const password = ref('')
|
|||||||
const error = ref('')
|
const error = ref('')
|
||||||
const pending = ref(false)
|
const pending = ref(false)
|
||||||
const session = useState('session')
|
const session = useState('session')
|
||||||
|
const route = useRoute()
|
||||||
const isDev = import.meta.dev
|
const isDev = import.meta.dev
|
||||||
|
const redirectPath = computed(() => typeof route.query.redirect === 'string' ? route.query.redirect : '/')
|
||||||
|
const authNotice = computed(() => {
|
||||||
|
if (route.query.reason === 'expired') return '로그인 세션이 만료되었습니다. 다시 로그인해 주세요.'
|
||||||
|
if (route.query.reason === 'required') return '가계부를 보려면 먼저 로그인해 주세요.'
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
|
const normalizedEmail = email.value.trim().toLowerCase()
|
||||||
|
if (!normalizedEmail || !normalizedEmail.includes('@')) {
|
||||||
|
error.value = '이메일 형식을 확인해 주세요.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (password.value.length < 8) {
|
||||||
|
error.value = '비밀번호는 8자 이상 입력해 주세요.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
pending.value = true
|
pending.value = true
|
||||||
error.value = ''
|
error.value = ''
|
||||||
try {
|
try {
|
||||||
const endpoint = mode.value === 'login' ? '/api/auth/login' : '/api/auth/register'
|
const endpoint = mode.value === 'login' ? '/api/auth/login' : '/api/auth/register'
|
||||||
session.value = await $fetch(endpoint, {
|
session.value = await $fetch(endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: { name: name.value, email: email.value, password: password.value }
|
body: { name: name.value, email: normalizedEmail, password: password.value }
|
||||||
})
|
})
|
||||||
await navigateTo('/')
|
await navigateTo(redirectPath.value)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const status = err?.statusCode || err?.response?.status
|
const status = err?.statusCode || err?.response?.status
|
||||||
const statusMessage = err?.statusMessage || err?.response?._data?.statusMessage || ''
|
const statusMessage = err?.statusMessage || err?.response?._data?.statusMessage || ''
|
||||||
@@ -97,6 +119,8 @@ async function submit() {
|
|||||||
: '가입 처리 중 문제가 생겼습니다. 잠시 후 다시 시도해 주세요.'
|
: '가입 처리 중 문제가 생겼습니다. 잠시 후 다시 시도해 주세요.'
|
||||||
} else if (mode.value === 'register') {
|
} else if (mode.value === 'register') {
|
||||||
error.value = '가입 처리 중 문제가 생겼습니다. 잠시 후 다시 시도해 주세요.'
|
error.value = '가입 처리 중 문제가 생겼습니다. 잠시 후 다시 시도해 주세요.'
|
||||||
|
} else if (!status) {
|
||||||
|
error.value = '서버에 연결할 수 없습니다. 네트워크 상태를 확인해 주세요.'
|
||||||
} else {
|
} else {
|
||||||
error.value = '이메일 또는 비밀번호를 확인해 주세요.'
|
error.value = '이메일 또는 비밀번호를 확인해 주세요.'
|
||||||
}
|
}
|
||||||
@@ -110,9 +134,12 @@ async function devLogin() {
|
|||||||
error.value = ''
|
error.value = ''
|
||||||
try {
|
try {
|
||||||
session.value = await $fetch('/api/auth/dev-login', { method: 'POST' })
|
session.value = await $fetch('/api/auth/dev-login', { method: 'POST' })
|
||||||
await navigateTo('/')
|
await navigateTo(redirectPath.value)
|
||||||
} catch {
|
} catch (err: any) {
|
||||||
error.value = '개발용 로그인에 실패했습니다.'
|
const status = err?.statusCode || err?.response?.status
|
||||||
|
error.value = status === 404
|
||||||
|
? '개발 환경에서만 사용할 수 있는 로그인입니다.'
|
||||||
|
: '개발용 로그인에 실패했습니다.'
|
||||||
} finally {
|
} finally {
|
||||||
pending.value = false
|
pending.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user