chore: init lms adm fe

This commit is contained in:
2026-07-07 17:23:40 +09:00
commit d682e9a382
57 changed files with 12065 additions and 0 deletions
+166
View File
@@ -0,0 +1,166 @@
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>
);
}