Files
money.sori.studio/pages/login.vue
2026-08-14 13:29:26 +09:00

152 lines
5.4 KiB
Vue

<template>
<section class="flex min-h-dvh flex-col justify-center px-5 py-6 md:mx-auto md:max-w-sm">
<div class="mb-6">
<p class="text-sm font-semibold text-brand-700">Money</p>
<h1 class="mt-2 text-2xl font-bold tracking-normal text-ink">빠르게 기록하는 개인 가계부</h1>
<p class="mt-2 text-sm leading-6 text-muted">계정을 만들면 내 자산, 카테고리, 거래 내역을 따로 관리할 수 있습니다.</p>
</div>
<div class="mb-4 grid grid-cols-2 gap-2">
<button
type="button"
class="segmented-button border border-line py-2.5"
:class="{ 'border-red-500 text-red-600': mode === 'login' }"
@click="mode = 'login'"
>
로그인
</button>
<button
type="button"
class="segmented-button border border-line py-2.5"
:class="{ 'border-red-500 text-red-600': mode === 'register' }"
@click="mode = 'register'"
>
회원가입
</button>
</div>
<form class="space-y-3" @submit.prevent="submit">
<div v-if="mode === 'register'">
<label class="label" for="name">이름</label>
<input id="name" v-model="name" class="field" autocomplete="name" placeholder="표시 이름">
</div>
<div>
<label class="label" for="email">이메일</label>
<input id="email" v-model="email" class="field" type="email" autocomplete="email" placeholder="you@example.com" required>
</div>
<div>
<label class="label" for="password">비밀번호</label>
<input
id="password"
v-model="password"
class="field"
type="password"
:autocomplete="mode === 'login' ? 'current-password' : 'new-password'"
placeholder="8자 이상"
required
>
</div>
<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">
{{ pending ? '확인 중...' : mode === 'login' ? '로그인' : '가입하고 시작하기' }}
</button>
</form>
<button
v-if="isDev"
type="button"
class="btn-secondary mt-3 w-full border-dashed text-muted"
:disabled="pending"
@click="devLogin"
>
개발용 테스트 로그인
</button>
</section>
</template>
<script setup lang="ts">
type AuthMode = 'login' | 'register'
const mode = ref<AuthMode>('login')
const name = ref('')
const email = ref('')
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: normalizedEmail, password: password.value }
})
await navigateTo(redirectPath.value)
} catch (err: any) {
const status = err?.statusCode || err?.response?.status
const statusMessage = err?.statusMessage || err?.response?._data?.statusMessage || ''
if (mode.value === 'register' && status === 409) {
error.value = '이미 가입된 이메일입니다.'
} else if (mode.value === 'register' && statusMessage) {
error.value = statusMessage === 'Invalid registration'
? '이메일과 8자 이상 비밀번호를 확인해 주세요.'
: '가입 처리 중 문제가 생겼습니다. 잠시 후 다시 시도해 주세요.'
} else if (mode.value === 'register') {
error.value = '가입 처리 중 문제가 생겼습니다. 잠시 후 다시 시도해 주세요.'
} else if (!status) {
error.value = '서버에 연결할 수 없습니다. 네트워크 상태를 확인해 주세요.'
} else {
error.value = '이메일 또는 비밀번호를 확인해 주세요.'
}
} finally {
pending.value = false
}
}
async function devLogin() {
pending.value = true
error.value = ''
try {
session.value = await $fetch('/api/auth/dev-login', { method: 'POST' })
await navigateTo(redirectPath.value)
} catch (err: any) {
const status = err?.statusCode || err?.response?.status
error.value = status === 404
? '개발 환경에서만 사용할 수 있는 로그인입니다.'
: '개발용 로그인에 실패했습니다.'
} finally {
pending.value = false
}
}
watch(mode, () => {
error.value = ''
})
</script>