167 lines
7.8 KiB
TypeScript
167 lines
7.8 KiB
TypeScript
import { useState, type FormEvent } from 'react';
|
|
import { ArrowRight, CheckCircle2 } from 'lucide-react';
|
|
import { BrandMark } from '../../components/BrandMark';
|
|
import { loginInstructor } from '../../lib/auth';
|
|
import type { AuthSession } from '../../types/auth';
|
|
|
|
type LoginPageProps = {
|
|
onLogin: (session: AuthSession) => void;
|
|
};
|
|
|
|
const REMEMBERED_EMAIL_STORAGE_KEY = 'atnmy.lms.admin.login.rememberedEmail';
|
|
const DEFAULT_EMAIL = '';
|
|
const DEFAULT_PASSWORD = 'qwer1234';
|
|
|
|
export function LoginPage({ onLogin }: LoginPageProps) {
|
|
const rememberedEmail = window.localStorage.getItem(REMEMBERED_EMAIL_STORAGE_KEY);
|
|
const [email, setEmail] = useState(rememberedEmail ?? DEFAULT_EMAIL);
|
|
const [password, setPassword] = useState(DEFAULT_PASSWORD);
|
|
const [rememberEmail, setRememberEmail] = useState(Boolean(rememberedEmail));
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState('');
|
|
|
|
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
|
event.preventDefault();
|
|
setLoading(true);
|
|
setError('');
|
|
|
|
try {
|
|
const session = await loginInstructor(email, password);
|
|
if (rememberEmail) {
|
|
window.localStorage.setItem(REMEMBERED_EMAIL_STORAGE_KEY, email);
|
|
} else {
|
|
window.localStorage.removeItem(REMEMBERED_EMAIL_STORAGE_KEY);
|
|
}
|
|
onLogin(session);
|
|
} catch (loginError) {
|
|
setError(loginError instanceof Error ? loginError.message : '로그인에 실패했습니다.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<main className="grid min-h-screen bg-white text-[#111827] lg:grid-cols-[minmax(420px,58%)_minmax(360px,42%)]">
|
|
<section className="relative flex min-h-[42vh] overflow-hidden bg-[linear-gradient(145deg,#3b82f6_0%,#7048e8_54%,#4f46e5_100%)] px-7 py-8 text-white sm:px-12 lg:min-h-screen lg:px-[60px]">
|
|
<div className="absolute inset-0 opacity-25">
|
|
<div className="absolute left-[14%] top-[18%] h-40 w-40 rounded-full bg-white blur-3xl" />
|
|
<div className="absolute bottom-[18%] right-[12%] h-56 w-56 rounded-full bg-[#93c5fd] blur-3xl" />
|
|
</div>
|
|
|
|
<div className="relative z-10 flex w-full max-w-[580px] flex-col">
|
|
<BrandMark />
|
|
|
|
<div className="my-auto py-12 lg:py-0">
|
|
<p className="mb-4 text-[11px] font-extrabold uppercase tracking-[0.16em] text-white/70">Learning Management System</p>
|
|
<h1 className="max-w-[520px] text-[34px] font-black leading-[1.12] tracking-[0] sm:text-[44px]">
|
|
<span className="text-[#67e8f9]">자율주행</span> 교육 플랫폼
|
|
</h1>
|
|
<p className="mt-5 max-w-[410px] text-[14px] font-bold leading-6 text-white/82">
|
|
실제 하드웨어와 시뮬레이션을 활용한 차세대 학습 관리 시스템
|
|
</p>
|
|
|
|
<div className="mt-8 grid max-w-[410px] gap-2.5 rounded-xl bg-white/12 p-4 ring-1 ring-white/18 backdrop-blur">
|
|
{['실시간 모니터링 및 통합 관리', '하드웨어 플랫폼 상태 추적', 'AI 가이드 에스컬레이션'].map((item) => (
|
|
<div className="flex items-center gap-3 text-[12px] font-bold text-white/88" key={item}>
|
|
<CheckCircle2 size={16} aria-hidden="true" />
|
|
{item}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<p className="text-[12px] font-bold text-white/55">© 2026. All rights reserved.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="flex min-h-[58vh] items-center justify-center px-7 py-10 sm:px-12 lg:min-h-screen">
|
|
<div className="w-full max-w-[420px]">
|
|
<div className="mb-8">
|
|
<h2 className="text-[26px] font-black tracking-[0] text-[#4f46e5]">강사 로그인</h2>
|
|
<p className="mt-2.5 text-[13px] font-bold text-[#6b7280]">이메일과 비밀번호를 입력해주세요.</p>
|
|
</div>
|
|
|
|
<form className="grid gap-4" onSubmit={handleSubmit}>
|
|
<label className="grid gap-2 text-[13px] font-extrabold text-[#303747]">
|
|
이메일
|
|
<input
|
|
className="h-11 rounded-lg border border-[#d8dde8] bg-white px-4 text-[14px] font-bold text-[#111827] outline-none transition placeholder:text-[#a0a7b7] focus:border-[#4f46e5] focus:ring-4 focus:ring-[#4f46e5]/12"
|
|
name="email"
|
|
placeholder="이메일을 입력하세요"
|
|
type="email"
|
|
value={email}
|
|
onChange={(event) => setEmail(event.target.value)}
|
|
required
|
|
/>
|
|
</label>
|
|
|
|
<label className="grid gap-2 text-[13px] font-extrabold text-[#303747]">
|
|
비밀번호
|
|
<input
|
|
className="h-11 rounded-lg border border-[#d8dde8] bg-white px-4 text-[14px] font-bold text-[#111827] outline-none transition placeholder:text-[#a0a7b7] focus:border-[#4f46e5] focus:ring-4 focus:ring-[#4f46e5]/12"
|
|
name="password"
|
|
placeholder="비밀번호를 입력하세요"
|
|
type="password"
|
|
value={password}
|
|
onChange={(event) => setPassword(event.target.value)}
|
|
required
|
|
/>
|
|
</label>
|
|
|
|
<div className="flex items-center justify-between gap-3">
|
|
<label className="flex items-center gap-2 text-[12px] font-bold text-[#64748b]">
|
|
<input
|
|
className="h-4 w-4 rounded border-[#d8dde8] accent-[#4f46e5]"
|
|
type="checkbox"
|
|
checked={rememberEmail}
|
|
onChange={(event) => setRememberEmail(event.target.checked)}
|
|
/>
|
|
아이디 기억
|
|
</label>
|
|
<button className="text-[12px] font-extrabold text-[#4f46e5]" type="button">
|
|
비밀번호를 잊으셨나요?
|
|
</button>
|
|
</div>
|
|
|
|
<button
|
|
className="h-12 rounded-lg bg-[#4f46e5] text-[14px] font-extrabold text-white shadow-[0_10px_24px_rgba(79,70,229,0.22)] transition hover:bg-[#4338ca] focus:outline-none focus:ring-4 focus:ring-[#4f46e5]/20 disabled:cursor-not-allowed disabled:opacity-70"
|
|
type="submit"
|
|
disabled={loading}
|
|
>
|
|
{loading ? '로그인 중...' : '로그인'}
|
|
</button>
|
|
|
|
{error ? <p className="text-center text-[12px] font-bold text-[#dc2626]">{error}</p> : null}
|
|
</form>
|
|
|
|
<div className="my-7 flex items-center gap-4">
|
|
<span className="h-px flex-1 bg-[#e3e7ef]" />
|
|
<span className="text-[12px] font-bold text-[#9aa2b1]">또는</span>
|
|
<span className="h-px flex-1 bg-[#e3e7ef]" />
|
|
</div>
|
|
|
|
<button
|
|
className="group flex h-12 w-full items-center justify-center gap-3 rounded-lg border border-[#d8dde8] bg-white px-4 text-[14px] font-extrabold text-[#263141] shadow-sm transition hover:border-[#b8c0d0] hover:bg-[#fafbff] focus:outline-none focus:ring-4 focus:ring-[#4f46e5]/15"
|
|
type="button"
|
|
onClick={() => setError('Google 로그인은 공통 인증 서버에 엔드포인트가 준비된 뒤 연결됩니다.')}
|
|
>
|
|
<GoogleMark />
|
|
Google로 계속
|
|
<ArrowRight className="ml-auto text-[#98a1b2] transition group-hover:translate-x-0.5 group-hover:text-[#4f46e5]" size={16} aria-hidden="true" />
|
|
</button>
|
|
|
|
<p className="mt-7 text-center text-[11px] font-bold leading-5 text-[#8b93a3]">접근 권한이 없는 계정은 관리자에게 문의하세요.</p>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
function GoogleMark() {
|
|
return (
|
|
<span className="grid h-5 w-5 place-items-center rounded-full border border-[#dbe1eb] bg-[#f8fafc] text-[15px] font-black leading-none text-[#4285f4]" aria-hidden="true">
|
|
<span>G</span>
|
|
</span>
|
|
);
|
|
}
|