Files
sori.studio/pages/signin.vue
zenn f5cd73b223 feat(member): 회원 설정/헤더 상태 UI와 관리자 멤버 관리 추가
로그인 상태를 헤더에서 즉시 인지하고 계정 관리를 이어갈 수 있도록 사용자 설정과 관리자 멤버 관측 기능을 연결했다.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 17:10:48 +09:00

130 lines
3.8 KiB
Vue

<script setup>
definePageMeta({
layout: 'page'
})
const isSubmitting = ref(false)
const errorMessage = ref('')
const statusMessage = ref('')
const showPassword = ref(false)
const form = reactive({
email: '',
password: ''
})
/**
* 로그인 입력값을 검증한다.
* @returns {boolean} 검증 통과 여부
*/
const validateSignIn = () => {
errorMessage.value = ''
statusMessage.value = ''
if (!form.email.trim() || !form.password) {
errorMessage.value = '이메일과 비밀번호를 입력해 주세요.'
return false
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) {
errorMessage.value = '이메일 형식이 올바르지 않습니다.'
return false
}
return true
}
/**
* 로그인 요청을 처리한다.
* @returns {Promise<void>}
*/
const submitSignIn = async () => {
if (!validateSignIn()) {
return
}
isSubmitting.value = true
try {
await $fetch('/api/auth/login', {
method: 'POST',
body: {
email: form.email.trim(),
password: form.password
}
})
statusMessage.value = '로그인되었습니다. 잠시 후 이동합니다.'
await navigateTo('/')
} catch (error) {
errorMessage.value = error?.data?.message || '로그인에 실패했습니다.'
} finally {
isSubmitting.value = false
}
}
</script>
<template>
<section class="auth-signin min-h-screen bg-[#0a0b0d] text-[#f5f7fa] [color-scheme:dark]">
<div class="mx-auto flex min-h-screen w-full max-w-[1280px] items-center px-5 py-12 sm:px-10 lg:px-16">
<div class="w-full max-w-[430px] p-5 sm:p-8">
<p class="text-2xl font-semibold leading-tight">
로그인
</p>
<p class="mt-2 text-sm text-[#9ba3af]">
가입한 이메일과 비밀번호로 로그인하세요.
</p>
<form class="mt-8 space-y-5" @submit.prevent="submitSignIn">
<div class="space-y-1.5">
<label class="text-xs text-[#d8dee6]">이메일</label>
<input
v-model="form.email"
class="auth-form-input h-10 w-full rounded-[8px] border border-[#1a212a] bg-transparent px-3 text-sm outline-none transition-colors focus:border-[#2f6feb]"
type="email"
autocomplete="email"
>
</div>
<div class="space-y-1.5">
<label class="text-xs text-[#d8dee6]">비밀번호</label>
<div class="flex items-center rounded-[8px] border border-[#1a212a] transition-colors focus-within:border-[#2f6feb]">
<input
v-model="form.password"
class="auth-form-input h-10 min-w-0 flex-1 bg-transparent px-3 text-sm outline-none"
:type="showPassword ? 'text' : 'password'"
autocomplete="current-password"
>
<AuthPasswordVisibilityToggle v-model="showPassword" />
</div>
</div>
<button
class="h-9 rounded-[8px] bg-[#2f6feb] px-8 text-xs font-medium text-white transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60"
type="submit"
:disabled="isSubmitting"
>
로그인
</button>
</form>
<p v-if="errorMessage" class="mt-4 text-xs text-[#e5acb1]" aria-live="polite">
{{ errorMessage }}
</p>
<p v-if="statusMessage" class="mt-4 text-xs text-[#9fc4ff]" aria-live="polite">
{{ statusMessage }}
</p>
<p class="mt-6 text-sm text-[#9ba3af]">
계정이 없으신가요?
<NuxtLink class="text-[#7eb8ff] hover:opacity-80" to="/signup">
회원가입
</NuxtLink>
</p>
<NuxtLink class="mt-2 inline-flex text-xs text-[#9ba3af] hover:opacity-80" to="/">
홈으로 돌아가기
</NuxtLink>
</div>
</div>
</section>
</template>