import React, { useState } from 'react' import { useNavigate } from 'react-router-dom' import { login, verifyOtp } from '../api/client' /** * FA 로그인 — UIWS/OCR 2FA(OTP) 레이어 이식. [GUARDiA-FA] * 1단계: 아이디/비밀번호 → /api/fa/auth/login (flat 응답). * - twofa="false": 즉시 { token, username, role, workstationCode } 발급(기존 단일 로그인 회귀 0). * - twofa="true": verifyMethod 로 2차 분기. * · OTP : Authenticator 6자리 → /verify-otp * · OTP_SETUP : QR(qrImage)+수동키(secret) 등록 후 6자리 → /verify-otp * 2단계: verify-token + 코드 → access 발급 → /dashboard. * 보안: OTP secret/QR·인증코드는 화면 표시용만 — 로그/저장 절대 금지. */ type VerifyMethod = 'OTP' | 'OTP_SETUP' export default function Login() { const [step, setStep] = useState<'login' | 'verify'>('login') const [username, setUsername] = useState('') const [password, setPassword] = useState('') const [code, setCode] = useState('') const [verifyToken, setVerifyToken] = useState('') const [verifyMethod, setVerifyMethod] = useState('OTP') const [qrImage, setQrImage] = useState('') // OTP_SETUP 등록 순간만 존재 const [secret, setSecret] = useState('') // OTP_SETUP 등록 순간만 존재 const [error, setError] = useState('') const [loading, setLoading] = useState(false) const navigate = useNavigate() const isSetup = verifyMethod === 'OTP_SETUP' // access 발급 후 세션 저장(기존 fa_token / fa_user 스키마 보존 — 회귀 0). const finish = (d: any) => { localStorage.setItem('fa_token', d.token) localStorage.setItem('fa_user', JSON.stringify({ username: d.username, role: d.role, workstationCode: d.workstationCode, })) navigate('/dashboard') } const submitLogin = async (e: React.FormEvent) => { e.preventDefault() setLoading(true) setError('') try { const res = await login(username, password) const d = res.data || {} if (d.twofa === 'true') { setVerifyToken(d.verifyToken || '') setVerifyMethod((d.verifyMethod as VerifyMethod) || 'OTP') setQrImage(d.qrImage || '') setSecret(d.secret || '') setCode('') setStep('verify') return } if (!d.token) throw new Error('no token') finish(d) } catch (err: any) { setError(err.response?.data?.error || err.response?.data?.message || '로그인 실패') } finally { setLoading(false) } } const submitVerify = async (e: React.FormEvent) => { e.preventDefault() setLoading(true) setError('') try { const res = await verifyOtp(verifyToken, code) const d = res.data || {} if (!d.token) throw new Error('no token') finish(d) } catch (err: any) { setError(err.response?.data?.error || err.response?.data?.message || '인증 실패 — 코드를 확인하세요.') } finally { setLoading(false) } } const backToLogin = () => { setStep('login'); setCode(''); setError('') setQrImage(''); setSecret('') // 시크릿 잔존 방지 } const inputCls = 'w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500' return (
GUARDiA FA
Factory Automation Platform
e-Paper · QR WIP · MES 통합 플랫폼
{step === 'login' ? (
setUsername(e.target.value)} className={inputCls} placeholder="username" autoComplete="username" required />
setPassword(e.target.value)} className={inputCls} placeholder="password" autoComplete="current-password" required />
{error &&
{error}
}
) : (
2차 인증
{isSetup ? (

최초 로그인입니다. 아래 QR을 Authenticator 앱(Google·Microsoft)으로 스캔해 등록한 뒤 6자리 코드를 입력하세요.

) : (

Authenticator 앱에 표시된 6자리 코드를 입력하세요.

)}
{isSetup && qrImage && (
OTP QR
)} {isSetup && secret && (
QR 스캔이 안 되면 수동 입력 키
{secret}
)}
setCode(e.target.value.replace(/\D/g, '').slice(0, 6))} inputMode="numeric" autoComplete="one-time-code" maxLength={6} autoFocus placeholder="000000" className={inputCls + ' tracking-[0.4em] text-center'} />
{error &&
{error}
}
)} {step === 'login' && (
기본 계정: admin / admin123
)}
) }