import { useEffect, useState, type FormEvent } from 'react'; import { CheckCircle2, Eye, EyeOff } from 'lucide-react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; 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 = ''; const AUTH_BASE_URL = (import.meta.env.VITE_ATNMY_AUTH_URL ?? import.meta.env.VITE_ADS_IDE_AUTH_URL ?? 'https://auth.autonomystudio.net').replace(/\/+$/, ''); const LAUNCHER_API_BASE_URL = (import.meta.env.VITE_LAUNCHER_API_BASE_URL ?? 'https://portal-api.autonomystudio.net/api').replace(/\/+$/, ''); const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 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 [showSignup, setShowSignup] = useState(false); const [signupNotice, setSignupNotice] = useState(''); const handleSubmit = async (event: FormEvent) => { 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 (

Learning Management System

자율주행 교육 플랫폼

실제 하드웨어와 시뮬레이션을 활용한 차세대 학습 관리 시스템

{['실시간 모니터링 및 통합 관리', '하드웨어 플랫폼 상태 추적', 'AI 가이드 에스컬레이션'].map((item) => (
))}

© 2026. All rights reserved.

{showSignup ? ( setShowSignup(false)} onComplete={(registeredEmail) => { setEmail(registeredEmail); setShowSignup(false); setSignupNotice('회원가입이 완료되었습니다. 강사 계정으로 로그인할 수 있습니다.'); }} /> ) : <>

강사 로그인

이메일과 비밀번호를 입력해주세요.

{error ?

{error}

: null} {signupNotice ?

{signupNotice}

: null}

강사 계정이 없으신가요?{' '}

접근 권한이 없는 계정은 관리자에게 문의하세요.

}
); } type InstructorSignupFormProps = { onCancel: () => void; onComplete: (email: string) => void; }; function InstructorSignupForm({ onCancel, onComplete }: InstructorSignupFormProps) { const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [passwordConfirm, setPasswordConfirm] = useState(''); const [instructorInviteCode, setInstructorInviteCode] = useState(''); const [verificationCode, setVerificationCode] = useState(''); const [verificationEmail, setVerificationEmail] = useState(''); const [codeSent, setCodeSent] = useState(false); const [codeTimer, setCodeTimer] = useState(0); const [sendingCode, setSendingCode] = useState(false); const [submitting, setSubmitting] = useState(false); const [showPassword, setShowPassword] = useState(false); const [showPasswordConfirm, setShowPasswordConfirm] = useState(false); const [agreeTerms, setAgreeTerms] = useState(false); const [agreePrivacy, setAgreePrivacy] = useState(false); const [error, setError] = useState(''); const [termModal, setTermModal] = useState<{ title: string; body: string; loading: boolean } | null>(null); const normalizedEmail = email.trim().toLowerCase(); const emailError = email.length > 0 && !EMAIL_PATTERN.test(email) ? '올바른 이메일 형식으로 입력해주세요.' : ''; useEffect(() => { if (codeTimer <= 0) return; const timer = window.setTimeout(() => setCodeTimer((value) => value - 1), 1000); return () => window.clearTimeout(timer); }, [codeTimer]); const handleEmailChange = (value: string) => { setEmail(value); if (verificationEmail && value.trim().toLowerCase() !== verificationEmail) { setCodeSent(false); setVerificationCode(''); setCodeTimer(0); } }; const sendVerificationCode = async () => { if (!normalizedEmail || emailError) return; setSendingCode(true); setError(''); try { const response = await fetch(`${AUTH_BASE_URL}/auth/email-verification/send`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ email: normalizedEmail }), }); if (response.status === 409) throw new Error('이미 가입된 이메일입니다.'); if (!response.ok) throw new Error('인증코드 발송에 실패했습니다.'); setCodeSent(true); setVerificationEmail(normalizedEmail); setVerificationCode(''); setCodeTimer(60); } catch (sendError) { setCodeSent(false); setError(sendError instanceof Error ? sendError.message : '인증코드 발송에 실패했습니다.'); } finally { setSendingCode(false); } }; const handleSubmit = async (event: FormEvent) => { event.preventDefault(); setError(''); if (!name.trim()) return setError('이름을 입력해주세요.'); if (emailError) return setError(emailError); if (password.length < 8) return setError('비밀번호는 8자 이상이어야 합니다.'); if (password !== passwordConfirm) return setError('비밀번호가 일치하지 않습니다.'); if (!agreeTerms || !agreePrivacy) return setError('필수 약관에 동의해주세요.'); if (!/^[A-Za-z0-9]{6}$/.test(instructorInviteCode.trim())) return setError('강사 가입 코드 6자리를 입력해주세요.'); if (!/^\d{6}$/.test(verificationCode)) return setError('이메일 인증코드 6자리를 입력해주세요.'); if (!codeSent || verificationEmail !== normalizedEmail) return setError('현재 이메일로 인증코드를 다시 발송해주세요.'); setSubmitting(true); try { const response = await fetch(`${AUTH_BASE_URL}/auth/instructor-signup`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ name: name.trim(), email: normalizedEmail, password, verificationCode, instructorInviteCode: instructorInviteCode.trim().toUpperCase() }), }); if (response.status === 409) throw new Error('이미 가입된 이메일입니다.'); if (response.status === 400) throw new Error('인증코드 또는 강사 가입 코드가 올바르지 않습니다.'); if (!response.ok) throw new Error('회원가입에 실패했습니다.'); onComplete(normalizedEmail); } catch (signupError) { setError(signupError instanceof Error ? signupError.message : '회원가입에 실패했습니다.'); } finally { setSubmitting(false); } }; const openTermModal = async (key: 'terms' | 'privacy') => { const title = key === 'terms' ? '이용약관' : '개인정보 처리방침'; setTermModal({ title, body: '', loading: true }); try { const response = await fetch(`${LAUNCHER_API_BASE_URL}/terms/${key}`); if (!response.ok) throw new Error('약관을 불러오지 못했습니다.'); const payload = await response.json() as { term?: { body?: string } }; setTermModal({ title, body: payload.term?.body || '등록된 약관 내용이 없습니다.', loading: false }); } catch (termError) { setTermModal({ title, body: termError instanceof Error ? termError.message : '약관을 불러오지 못했습니다.', loading: false, }); } }; return ( <>

강사 회원가입

강사 가입 코드를 입력해주세요.

이메일
handleEmailChange(event.target.value)} placeholder="이메일을 입력하세요" required />
{emailError ?

{emailError}

: null}
setVerificationCode(value.replace(/\D/g, '').slice(0, 6))} placeholder="이메일 인증코드 6자리를 입력하세요" inputMode="numeric" /> {codeSent ?

인증코드가 이메일로 발송되었습니다.

: null} setInstructorInviteCode(value.replace(/[^a-z0-9]/gi, '').toUpperCase().slice(0, 6))} placeholder="조직에서 받은 6자리 코드를 입력하세요" /> setShowPassword((value) => !value)} /> setShowPasswordConfirm((value) => !value)} />
{error ?

{error}

: null} {termModal ?

{termModal.title}