운영 HTTP에서 관리자 세션이 유지되지 않던 문제를 쿠키 공통화로 수정하고, 통계 클라이언트 분리·조회 오류·기간별 차트를 보강했다. 방문자 해시는 32일 초과분만 정리하고 일별 집계는 누적 보관한다. Co-authored-by: Cursor <cursoragent@cursor.com>
149 lines
4.4 KiB
Vue
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>
|