diff --git a/docs/changelog.md b/docs/changelog.md index b03f043..abcfcf6 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -86,3 +86,5 @@ - 내역 헤더의 검색과 필터 버튼을 `assets/icons/search.svg`, `assets/icons/tune.svg` 인라인 SVG로 교체했다. - CSV 내보내기 컬럼을 한글 컬럼명과 앱의 거래 흐름에 맞는 순서로 정리하고 Excel 호환 BOM을 추가했다. - 빈 데이터 상태 화면을 공통 컴포넌트로 분리하고 내역, 통계, 자산, 설정, 공유 화면에 적용했다. +- 로그인 화면에 세션 만료/로그인 필요 안내를 추가하고, 로그인 후 원래 접근하려던 화면으로 돌아가도록 했다. +- 로그인/회원가입 입력값을 요청 전에 먼저 검증하고 네트워크 오류 메시지를 더 명확하게 표시하도록 했다. diff --git a/docs/project-guidelines.md b/docs/project-guidelines.md index c5bc43f..98dee71 100644 --- a/docs/project-guidelines.md +++ b/docs/project-guidelines.md @@ -56,3 +56,4 @@ user.email: zenn.message@gmail.com - 의미 있는 수정 단위마다 변경 내역을 문서에 남기고 커밋/푸시한다. - 원격 저장소는 `https://git.sori.studio/zenn/money.sori.studio.git`를 사용한다. +- 인증이 필요한 화면에서 로그인 화면으로 이동할 때는 로그인 필요/세션 만료처럼 사용자가 이유를 이해할 수 있는 안내를 함께 제공한다. diff --git a/middleware/auth.global.ts b/middleware/auth.global.ts index e27f197..659841b 100644 --- a/middleware/auth.global.ts +++ b/middleware/auth.global.ts @@ -2,6 +2,8 @@ export default defineNuxtRouteMiddleware(async (to) => { if (to.path === '/login' || to.path.startsWith('/share/')) return const session = useState<{ id: string; email: string; name: string } | null>('session') + const sessionCookie = useCookie('money_session') + const authReason = sessionCookie.value ? 'expired' : 'required' if (session.value === undefined) { try { @@ -12,6 +14,12 @@ export default defineNuxtRouteMiddleware(async (to) => { } if (!session.value) { - return navigateTo('/login') + return navigateTo({ + path: '/login', + query: { + reason: authReason, + redirect: to.fullPath === '/' ? undefined : to.fullPath + } + }) } }) diff --git a/pages/login.vue b/pages/login.vue index 854380f..a56601b 100644 --- a/pages/login.vue +++ b/pages/login.vue @@ -46,7 +46,12 @@ required > -

{{ error }}

+

+ {{ authNotice }} +

+

+ {{ error }} +

@@ -74,18 +79,35 @@ const password = ref('') const error = ref('') const pending = ref(false) const session = useState('session') +const route = useRoute() 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() { + 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 error.value = '' try { const endpoint = mode.value === 'login' ? '/api/auth/login' : '/api/auth/register' session.value = await $fetch(endpoint, { 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) { const status = err?.statusCode || err?.response?.status const statusMessage = err?.statusMessage || err?.response?._data?.statusMessage || '' @@ -97,6 +119,8 @@ async function submit() { : '가입 처리 중 문제가 생겼습니다. 잠시 후 다시 시도해 주세요.' } else if (mode.value === 'register') { error.value = '가입 처리 중 문제가 생겼습니다. 잠시 후 다시 시도해 주세요.' + } else if (!status) { + error.value = '서버에 연결할 수 없습니다. 네트워크 상태를 확인해 주세요.' } else { error.value = '이메일 또는 비밀번호를 확인해 주세요.' } @@ -110,9 +134,12 @@ async function devLogin() { error.value = '' try { session.value = await $fetch('/api/auth/dev-login', { method: 'POST' }) - await navigateTo('/') - } catch { - error.value = '개발용 로그인에 실패했습니다.' + await navigateTo(redirectPath.value) + } catch (err: any) { + const status = err?.statusCode || err?.response?.status + error.value = status === 404 + ? '개발 환경에서만 사용할 수 있는 로그인입니다.' + : '개발용 로그인에 실패했습니다.' } finally { pending.value = false } diff --git a/todo.md b/todo.md index 71e2aba..ef547b3 100644 --- a/todo.md +++ b/todo.md @@ -35,7 +35,6 @@ ## 7. 인증과 사용자 경험 - 회원 탈퇴 또는 데이터 삭제 정책을 정한다. -- 로그인 실패, 세션 만료, 네트워크 오류 상태를 화면에서 더 명확히 보여준다. ## 8. 공유