150 lines
3.5 KiB
Vue
150 lines
3.5 KiB
Vue
<script setup>
|
|
import { onMounted, ref } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { useAuthStore } from '../stores/auth'
|
|
import { api } from '../lib/api'
|
|
|
|
const router = useRouter()
|
|
const auth = useAuthStore()
|
|
|
|
const email = ref('')
|
|
const password = ref('')
|
|
const mode = ref('login')
|
|
const error = ref('')
|
|
const hasUsers = ref(true)
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
const meta = await api.authMeta()
|
|
hasUsers.value = !!meta.hasUsers
|
|
} catch (e) {
|
|
hasUsers.value = true
|
|
}
|
|
})
|
|
|
|
async function submit() {
|
|
error.value = ''
|
|
try {
|
|
if (mode.value === 'signup') await auth.signup(email.value, password.value)
|
|
else await auth.login(email.value, password.value)
|
|
router.push('/me')
|
|
} catch (e) {
|
|
error.value = '로그인/회원가입에 실패했어요.'
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<section class="wrap">
|
|
<form class="card" @submit.prevent="submit">
|
|
<div class="tabs">
|
|
<button type="button" class="tab" :class="{ 'tab--active': mode === 'login' }" @click="mode = 'login'">
|
|
로그인
|
|
</button>
|
|
<button type="button" class="tab" :class="{ 'tab--active': mode === 'signup' }" @click="mode = 'signup'">
|
|
회원가입
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="error" class="error">{{ error }}</div>
|
|
|
|
<label class="label">이메일</label>
|
|
<input v-model="email" class="input" placeholder="you@example.com" autocomplete="email" />
|
|
|
|
<label class="label">비밀번호</label>
|
|
<input
|
|
v-model="password"
|
|
class="input"
|
|
type="password"
|
|
placeholder="********"
|
|
autocomplete="current-password"
|
|
/>
|
|
|
|
<button class="btn" type="submit">{{ mode === 'signup' ? '회원가입' : '로그인' }}</button>
|
|
|
|
<div v-if="!hasUsers" class="hint">첫 회원가입 계정은 자동으로 admin 권한이 부여됩니다(개발용).</div>
|
|
</form>
|
|
</section>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.wrap {
|
|
min-height: calc(100vh - 74px);
|
|
display: grid;
|
|
place-items: center;
|
|
padding: 14px 2px;
|
|
}
|
|
.card {
|
|
max-width: 420px;
|
|
width: min(420px, 92vw);
|
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
background: rgba(255, 255, 255, 0.04);
|
|
border-radius: 16px;
|
|
padding: 14px;
|
|
box-sizing: border-box;
|
|
}
|
|
.tabs {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 8px;
|
|
margin-bottom: 10px;
|
|
}
|
|
.tab {
|
|
padding: 10px 12px;
|
|
border-radius: 12px;
|
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
background: rgba(0, 0, 0, 0.12);
|
|
color: rgba(255, 255, 255, 0.9);
|
|
font-weight: 800;
|
|
cursor: pointer;
|
|
}
|
|
.tab--active {
|
|
background: rgba(96, 165, 250, 0.18);
|
|
border-color: rgba(255, 255, 255, 0.16);
|
|
}
|
|
.error {
|
|
margin-bottom: 10px;
|
|
padding: 10px 12px;
|
|
border-radius: 12px;
|
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
|
background: rgba(239, 68, 68, 0.12);
|
|
}
|
|
.label {
|
|
display: block;
|
|
font-size: 13px;
|
|
opacity: 0.78;
|
|
margin-top: 10px;
|
|
margin-bottom: 6px;
|
|
}
|
|
.input {
|
|
width: 100%;
|
|
padding: 10px 12px;
|
|
border-radius: 12px;
|
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
background: rgba(0, 0, 0, 0.18);
|
|
color: rgba(255, 255, 255, 0.92);
|
|
outline: none;
|
|
box-sizing: border-box;
|
|
}
|
|
.btn {
|
|
margin-top: 12px;
|
|
width: 100%;
|
|
padding: 10px 12px;
|
|
border-radius: 12px;
|
|
border: 1px solid rgba(255, 255, 255, 0.14);
|
|
background: rgba(96, 165, 250, 0.2);
|
|
color: rgba(255, 255, 255, 0.92);
|
|
cursor: pointer;
|
|
font-weight: 800;
|
|
}
|
|
.btn:hover {
|
|
background: rgba(96, 165, 250, 0.26);
|
|
}
|
|
.hint {
|
|
margin-top: 10px;
|
|
opacity: 0.72;
|
|
font-size: 13px;
|
|
}
|
|
</style>
|
|
|