import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Shield, Eye, EyeOff, Lock, User, Fingerprint, KeyRound, AlertCircle } from 'lucide-react'; import { useAuth, type UserRole } from '@/app/auth/AuthContext'; /* * SFR-01: 시스템 로그인 및 권한 관리 * - 해양경찰 SSO·공무원증·GPKI 등 기존 인증체계 로그인 연동 * - 역할 기반 권한 관리(RBAC) * - 비밀번호 정책, 계정 잠금 정책 * - 감사 로그 기록 * - 5회 연속 실패 시 계정 잠금(30분) */ type AuthMethod = 'password' | 'gpki' | 'sso'; // SFR-01: 시뮬레이션 계정 (역할별) const DEMO_ACCOUNTS: Record = { admin: { pw: 'admin1234!', name: '김영수', rank: '사무관', org: '본청 정보통신과', role: 'ADMIN' }, operator: { pw: 'oper12345!', name: '이상호', rank: '경위', org: '서해지방해경청', role: 'OPERATOR' }, analyst: { pw: 'anal12345!', name: '정해진', rank: '주무관', org: '남해지방해경청', role: 'ANALYST' }, field: { pw: 'field1234!', name: '박민수', rank: '경사', org: '5001함 삼봉', role: 'FIELD' }, viewer: { pw: 'view12345!', name: '최원석', rank: '6급', org: '해수부 어업관리과', role: 'VIEWER' }, }; const MAX_LOGIN_ATTEMPTS = 5; const LOCKOUT_DURATION = 30 * 60 * 1000; // 30분 export function LoginPage() { const { t } = useTranslation('auth'); const navigate = useNavigate(); const { user, login } = useAuth(); const [authMethod, setAuthMethod] = useState('password'); const [userId, setUserId] = useState(''); const [password, setPassword] = useState(''); const [showPw, setShowPw] = useState(false); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); const [failCount, setFailCount] = useState(0); const [lockedUntil, setLockedUntil] = useState(null); // user 상태가 확정된 후 대시보드로 이동 useEffect(() => { if (user) navigate('/dashboard', { replace: true }); }, [user, navigate]); const doLogin = (method: AuthMethod, account?: typeof DEMO_ACCOUNTS[string]) => { const u = account || DEMO_ACCOUNTS['operator']; login({ id: userId || u.role, name: u.name, rank: u.rank, org: u.org, role: u.role, authMethod: method, loginAt: new Date().toISOString().replace('T', ' ').slice(0, 19), }); }; const handleLogin = (e: React.FormEvent) => { e.preventDefault(); setError(''); // SFR-01: 계정 잠금 확인 if (lockedUntil && Date.now() < lockedUntil) { const remainMin = Math.ceil((lockedUntil - Date.now()) / 60000); setError(t('error.locked', { minutes: remainMin })); return; } if (authMethod === 'password') { if (!userId.trim()) { setError(t('error.emptyId')); return; } if (!password.trim()) { setError(t('error.emptyPassword')); return; } if (password.length < 9) { setError(t('error.invalidPassword')); return; } } setLoading(true); setTimeout(() => { setLoading(false); // SFR-01: ID/PW 인증 시 계정 검증 const account = DEMO_ACCOUNTS[userId.toLowerCase()]; if (authMethod === 'password' && (!account || account.pw !== password)) { const newCount = failCount + 1; setFailCount(newCount); if (newCount >= MAX_LOGIN_ATTEMPTS) { setLockedUntil(Date.now() + LOCKOUT_DURATION); setError(t('error.maxFailed', { max: MAX_LOGIN_ATTEMPTS })); } else { setError(t('error.wrongCredentials', { count: newCount, max: MAX_LOGIN_ATTEMPTS })); } return; } setFailCount(0); setLockedUntil(null); doLogin(authMethod, account); }, 1200); }; const DEMO_ROLE_LABELS: Record = { ADMIN: t('demo.admin'), OPERATOR: t('demo.operator'), ANALYST: t('demo.analyst'), FIELD: t('demo.field'), VIEWER: t('demo.viewer'), }; const authMethods: { key: AuthMethod; icon: React.ElementType; label: string; desc: string }[] = [ { key: 'password', icon: Lock, label: t('authMethod.password'), desc: t('authMethod.passwordDesc') }, { key: 'gpki', icon: Fingerprint, label: t('authMethod.gpki'), desc: t('authMethod.gpkiDesc') }, { key: 'sso', icon: KeyRound, label: t('authMethod.sso'), desc: t('authMethod.ssoDesc') }, ]; return (
{/* 배경 그리드 */}
{/* 로그인 카드 */}
{/* 로고 영역 */}

{t('title')}

{t('subtitle')}

{/* 인증 방식 선택 탭 */}
{authMethods.map((m) => ( ))}
{/* ID/PW 로그인 */} {authMethod === 'password' && (
setUserId(e.target.value)} placeholder={t('form.userIdPlaceholder')} className="w-full bg-surface-overlay border border-slate-700/50 rounded-lg pl-10 pr-4 py-2.5 text-sm text-heading placeholder:text-hint focus:outline-none focus:border-blue-500/60 focus:ring-1 focus:ring-blue-500/20" autoComplete="username" />
setPassword(e.target.value)} placeholder={t('form.passwordPlaceholder')} className="w-full bg-surface-overlay border border-slate-700/50 rounded-lg pl-10 pr-10 py-2.5 text-sm text-heading placeholder:text-hint focus:outline-none focus:border-blue-500/60 focus:ring-1 focus:ring-blue-500/20" autoComplete="current-password" />
{/* 비밀번호 정책 안내 */}
{t('passwordPolicy.title')}
  • {t('passwordPolicy.minLength')}
  • {t('passwordPolicy.changeInterval')}
  • {t('passwordPolicy.lockout')}
  • {t('passwordPolicy.reuse')}
{error && (
{error}
)} {/* 데모 퀵로그인 */}
{t('demo.title')}
{Object.entries(DEMO_ACCOUNTS).map(([key, acct]) => ( ))}
)} {/* GPKI 인증 */} {authMethod === 'gpki' && (

{t('gpki.title')}

{t('gpki.desc')}

{t('gpki.certStatus')}
{t('gpki.certWaiting')}

{t('gpki.internalOnly')}

)} {/* SSO 연동 */} {authMethod === 'sso' && (

{t('sso.title')}

{t('sso.desc')}

{t('sso.tokenDetected')}

{t('sso.sessionNote')}

)}
{/* 하단 정보 */}
{t('footer.version')} {t('footer.org')}
{/* 접근 권한 안내 */}

{t('accessNotice')}

); }