(null)
+
+ const afterAuthenticated = async () => {
+ const me = await api.get('/auth/me')
+ localStorage.setItem('hrm_user', JSON.stringify(me.data.data))
+ nav('/dashboard')
+ }
+
const login = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true); setError('')
try {
const { data } = await api.post('/auth/login', form)
+ const payload = data.data || {}
+ if (payload.twofa === 'true') {
+ // 2FA 활성 — verifyMethod 로 분기(access 토큰 미발급). 시크릿/QR은 화면 표시용만.
+ setTwofa({
+ verifyToken: payload.verifyToken,
+ verifyMethod: (payload.verifyMethod as VerifyMethod) || 'EMAIL',
+ maskedEmail: payload.maskedEmail || '',
+ qrImage: payload.qrImage || '',
+ secret: payload.secret || '',
+ })
+ setCode('')
+ return
+ }
+ localStorage.setItem('hrm_token', payload.token)
+ await afterAuthenticated()
+ } catch (err: any) {
+ const msg = err?.response?.data?.message || ''
+ setError(msg.includes('잠겼') || msg.includes('승인')
+ ? msg : '아이디 또는 비밀번호가 올바르지 않습니다.')
+ } finally { setLoading(false) }
+ }
+
+ const verify = async (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!twofa) return
+ setLoading(true); setError('')
+ try {
+ // OTP·OTP_SETUP 은 /verify-otp, 이메일은 /verify(하위호환)
+ const { data } = isOtp
+ ? await verifyOtp(twofa.verifyToken, code)
+ : await verify2fa(twofa.verifyToken, code)
localStorage.setItem('hrm_token', data.data.token)
- const me = await api.get('/auth/me')
- localStorage.setItem('hrm_user', JSON.stringify(me.data.data))
- nav('/dashboard')
+ await afterAuthenticated()
} catch {
- setError('아이디 또는 비밀번호가 올바르지 않습니다.')
+ setError('인증 코드가 올바르지 않거나 만료되었습니다.')
} finally { setLoading(false) }
}
@@ -29,31 +83,154 @@ export default function LoginPage() {
GUARDiA HRM
AI 기반 통합 인사관리 플랫폼
-
+
+ {!twofa ? (
+
+ ) : (
+
+ )}
+
+ {helper && setHelper(null)} />}
+
+ )
+}
+
+/** 로그인 보조 3종 모달 — 회원가입 / 아이디찾기 / 비밀번호 초기화. */
+function HelperModal({ kind, onClose }: { kind: Exclude; onClose: () => void }) {
+ const [f, setF] = useState({ username: '', password: '', displayName: '', email: '' })
+ const [msg, setMsg] = useState('')
+ const [busy, setBusy] = useState(false)
+ const set = (k: string, v: string) => setF({ ...f, [k]: v })
+
+ const TITLE: Record = { signup: '회원가입', findId: '아이디 찾기', resetPw: '비밀번호 초기화' }
+
+ const submit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setBusy(true); setMsg('')
+ try {
+ if (kind === 'signup') {
+ const { data } = await signup({ username: f.username, password: f.password, displayName: f.displayName, email: f.email })
+ setMsg(data.data?.message || '처리되었습니다.')
+ } else if (kind === 'findId') {
+ const { data } = await findId({ displayName: f.displayName, email: f.email })
+ setMsg(data.data?.found ? `회원님의 아이디: ${data.data.maskedUsername}` : '일치하는 계정을 찾을 수 없습니다.')
+ } else {
+ const { data } = await resetPasswordHelper({ username: f.username, email: f.email })
+ setMsg(data.data?.message || '처리되었습니다.')
+ }
+ } catch {
+ setMsg('요청 처리 중 오류가 발생했습니다.')
+ } finally { setBusy(false) }
+ }
+
+ const input = (k: string, ph: string, type = 'text') => (
+ set(k, e.target.value)}
+ className="w-full border border-slate-200 rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
+ placeholder={ph} required />
+ )
+
+ return (
+
)
}