Files
sori.studio/pages/admin/login.vue
zenn c43873ce5f v1.3.5: 관리자 로그인·대시보드 차트·통계 보관 정리
운영 HTTP에서 관리자 세션이 유지되지 않던 문제를 쿠키 공통화로 수정하고, 통계 클라이언트 분리·조회 오류·기간별 차트를 보강했다. 방문자 해시는 32일 초과분만 정리하고 일별 집계는 누적 보관한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 13:54:38 +09:00

149 lines
4.4 KiB
Vue

<script setup>
definePageMeta({
layout: false
})
const form = reactive({
email: '',
password: ''
})
const pending = ref(false)
const errorMessage = ref('')
const emailInput = ref(null)
const passwordInput = ref(null)
const { data: bootstrapStatus } = await useFetch('/api/auth/bootstrap-status', {
default: () => ({
hasUsers: true,
needsAdminSetup: false
})
})
/**
* 브라우저/비밀번호 관리자 자동완성 값을 로그인 폼 상태에 반영한다.
* @returns {void}
*/
const syncAdminLoginAutofill = () => {
const emailValue = emailInput.value?.value || ''
const passwordValue = passwordInput.value?.value || ''
if (emailValue && emailValue !== form.email) {
form.email = emailValue
}
if (passwordValue && passwordValue !== form.password) {
form.password = passwordValue
}
}
/**
* 관리자 로그인 제출
* @returns {Promise<void>} 로그인 처리 결과
*/
const submitLogin = async () => {
syncAdminLoginAutofill()
pending.value = true
errorMessage.value = ''
try {
await $fetch('/admin/api/auth/login', {
method: 'POST',
body: {
email: form.email.trim(),
password: form.password
},
credentials: 'include'
})
if (import.meta.client) {
window.location.assign('/admin')
return
}
await navigateTo('/admin')
} catch (error) {
const statusCode = Number(error?.statusCode || error?.response?.status || 0)
if (statusCode === 401) {
errorMessage.value = '이메일 또는 비밀번호를 확인해 주세요.'
} else if (statusCode >= 500) {
errorMessage.value = '서버 또는 데이터베이스 연결을 확인해 주세요.'
} else {
errorMessage.value = '로그인에 실패했습니다. 잠시 후 다시 시도해 주세요.'
}
} finally {
pending.value = false
}
}
onMounted(() => {
syncAdminLoginAutofill()
window.setTimeout(syncAdminLoginAutofill, 100)
window.setTimeout(syncAdminLoginAutofill, 500)
})
</script>
<template>
<main class="admin-login flex min-h-screen items-center justify-center bg-[#f5f5f2] px-5 text-ink">
<section class="admin-login__panel w-full max-w-sm border border-line bg-paper p-8">
<p class="admin-login__eyebrow text-xs font-semibold uppercase text-muted">
Admin
</p>
<h1 class="admin-login__title mt-2 text-3xl font-semibold">
로그인
</h1>
<p
v-if="bootstrapStatus?.needsAdminSetup"
class="mt-3 rounded border border-[#ff4f2e]/30 bg-[#ff4f2e]/10 px-3 py-2 text-xs text-[#b63a23]"
>
등록된 관리자가 없습니다.
<NuxtLink class="font-semibold underline-offset-2 hover:underline" to="/signup">
관리자 등록으로 이동
</NuxtLink>
</p>
<form class="admin-login__form mt-8 grid gap-4" @submit.prevent="submitLogin">
<label class="admin-login__field grid gap-2 text-sm">
<span class="admin-login__label font-medium">이메일</span>
<input
ref="emailInput"
v-model="form.email"
class="admin-login__input rounded border border-line bg-white px-3 py-2"
type="email"
autocomplete="username"
required
@change="syncAdminLoginAutofill"
@focus="syncAdminLoginAutofill"
@input="syncAdminLoginAutofill"
>
</label>
<label class="admin-login__field grid gap-2 text-sm">
<span class="admin-login__label font-medium">비밀번호</span>
<input
ref="passwordInput"
v-model="form.password"
class="admin-login__input rounded border border-line bg-white px-3 py-2"
type="password"
autocomplete="current-password"
required
@change="syncAdminLoginAutofill"
@focus="syncAdminLoginAutofill"
@input="syncAdminLoginAutofill"
>
</label>
<p v-if="errorMessage" class="admin-login__error text-sm text-red-600">
{{ errorMessage }}
</p>
<button
class="admin-login__button rounded bg-[#15171a] px-4 py-2 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-50"
type="submit"
:disabled="pending"
>
{{ pending ? '확인 중' : '로그인' }}
</button>
</form>
</section>
</main>
</template>