170 lines
6.7 KiB
PHP
170 lines
6.7 KiB
PHP
@extends('admin.layouts.auth')
|
|
@section('hide_flash', '1')
|
|
@section('title', '로그인')
|
|
|
|
{{-- reCAPTCHA 스크립트는 이 페이지에서만 로드 --}}
|
|
@push('head')
|
|
@php
|
|
$siteKey = (string) config('services.recaptcha.site_key');
|
|
@endphp
|
|
|
|
@if($siteKey)
|
|
<script>window.__recaptchaSiteKey = @json($siteKey);</script>
|
|
<script src="https://www.google.com/recaptcha/api.js?render={{ $siteKey }}"></script>
|
|
<script src="{{ asset('assets/js/recaptcha-v3.js') }}"></script>
|
|
@endif
|
|
@endpush
|
|
|
|
@section('content')
|
|
<form id="loginForm" method="POST" action="{{ route('admin.login.store') }}" class="a-form" novalidate>
|
|
@csrf
|
|
<input type="hidden" name="g-recaptcha-response" id="g-recaptcha-response" value="">
|
|
|
|
{{-- 에러는 폼 상단에 1개만 --}}
|
|
@if ($errors->any())
|
|
<div class="a-alert a-alert--danger" style="margin-bottom:12px;">
|
|
<div class="a-alert__title">로그인 실패</div>
|
|
<div class="a-alert__body">{{ $errors->first() }}</div>
|
|
</div>
|
|
@endif
|
|
|
|
<div class="a-field">
|
|
<label class="a-label" for="login_id">아이디(이메일)</label>
|
|
<input class="a-input" id="login_id" name="login_id" type="text"
|
|
autocomplete="username" autofocus value="{{ old('login_id') }}">
|
|
</div>
|
|
|
|
<div class="a-field">
|
|
<label class="a-label" for="password">비밀번호</label>
|
|
<input class="a-input" id="password" name="password" type="password"
|
|
autocomplete="current-password">
|
|
</div>
|
|
|
|
|
|
<button class="a-btn a-btn--primary" type="submit" style="margin-top:14px;">로그인</button>
|
|
<div class="a-help" style="margin-top:10px;">
|
|
<small class="a-muted">로그인 성공 후 SMS 인증번호 입력 단계로 이동합니다.</small>
|
|
</div>
|
|
</form>
|
|
@endsection
|
|
|
|
@push('scripts')
|
|
<script>
|
|
(function () {
|
|
const form = document.getElementById('loginForm');
|
|
if (!form) return;
|
|
|
|
const emailEl = document.getElementById('login_id');
|
|
const pwEl = document.getElementById('password');
|
|
const btn = form.querySelector('button[type="submit"]');
|
|
|
|
// web 공통 showMsg가 있으면 사용, 없으면 alert로 fallback
|
|
const showMsgSafe = async (msg, opt) => {
|
|
if (typeof window.showMsg === 'function') return window.showMsg(msg, opt);
|
|
alert(msg);
|
|
};
|
|
|
|
const isEmail = (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
|
|
|
|
function ensureHiddenRecaptcha(){
|
|
let el = document.getElementById('g-recaptcha-response');
|
|
if (!el) {
|
|
el = document.createElement('input');
|
|
el.type = 'hidden';
|
|
el.id = 'g-recaptcha-response';
|
|
el.name = 'g-recaptcha-response';
|
|
form.appendChild(el);
|
|
}
|
|
return el;
|
|
}
|
|
|
|
async function getRecaptchaToken(action){
|
|
// 1) 프로젝트 공통 함수 우선
|
|
if (typeof window.recaptchaV3Exec === 'function') {
|
|
try {
|
|
const t = await window.recaptchaV3Exec(action);
|
|
return t || '';
|
|
} catch (e) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
// 2) fallback: grecaptcha.execute
|
|
const siteKey = window.__recaptchaSiteKey || '';
|
|
if (!siteKey) return '';
|
|
if (typeof window.grecaptcha === 'undefined') return '';
|
|
|
|
try {
|
|
await new Promise(r => window.grecaptcha.ready(r));
|
|
const t = await window.grecaptcha.execute(siteKey, { action });
|
|
return t || '';
|
|
} catch (e) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
form.addEventListener('submit', async function (e) {
|
|
e.preventDefault();
|
|
|
|
const email = (emailEl?.value || '').trim();
|
|
const pw = (pwEl?.value || '');
|
|
|
|
if (!email) {
|
|
await showMsgSafe('아이디(이메일)를 입력해 주세요.', { type: 'alert', title: '폼체크' });
|
|
emailEl?.focus();
|
|
return;
|
|
}
|
|
if (!isEmail(email)) {
|
|
await showMsgSafe('아이디는 이메일 형식이어야 합니다.', { type: 'alert', title: '폼체크' });
|
|
emailEl?.focus();
|
|
return;
|
|
}
|
|
if (!pw) {
|
|
await showMsgSafe('비밀번호를 입력해 주세요.', { type: 'alert', title: '폼체크' });
|
|
pwEl?.focus();
|
|
return;
|
|
}
|
|
|
|
if (btn) btn.disabled = true;
|
|
|
|
try {
|
|
const isProd = @json(app()->environment('production'));
|
|
const hasKey = @json((bool) config('services.recaptcha.site_key'));
|
|
|
|
// 운영에서만 토큰 생성 (서버와 동일 정책)
|
|
if (isProd && hasKey) {
|
|
const hidden = ensureHiddenRecaptcha();
|
|
hidden.value = '';
|
|
|
|
const token = await getRecaptchaToken('admin_login');
|
|
hidden.value = token || '';
|
|
|
|
if (!hidden.value) {
|
|
if (btn) btn.disabled = false;
|
|
await showMsgSafe(
|
|
'보안 검증(reCAPTCHA) 토큰 생성에 실패했습니다. 새로고침 후 다시 시도해 주세요.',
|
|
{ type: 'alert', title: '보안검증 실패' }
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
form.submit();
|
|
} catch (err) {
|
|
if (btn) btn.disabled = false;
|
|
await showMsgSafe('로그인 처리 중 오류가 발생했습니다.hhh 잠시 후 다시 시도해 주세요.', { type: 'alert', title: '오류' });
|
|
}
|
|
});
|
|
|
|
// 서버 validation 에러가 있으면 한 번 띄우기(선택)
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
const msg =
|
|
@json($errors->first('login_id') ?: $errors->first('password') ?: '');
|
|
if (msg) {
|
|
await showMsgSafe(msg, { type: 'alert', title: '로그인 실패' });
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
@endpush
|