diff options
Diffstat (limited to 'components/login')
| -rw-r--r-- | components/login/login-form.tsx | 290 |
1 files changed, 213 insertions, 77 deletions
diff --git a/components/login/login-form.tsx b/components/login/login-form.tsx index d249b6b1..0e047cc7 100644 --- a/components/login/login-form.tsx +++ b/components/login/login-form.tsx @@ -20,6 +20,7 @@ import { InputOTPSlot, } from "@/components/ui/input-otp" import { requestPasswordResetAction } from "@/lib/users/auth/partners-auth"; +import { checkEmailAndStartAuth, resendEmailOtp } from "@/lib/users/auth/email-auth"; import Loading from "../common/loading/loading"; type LoginMethod = 'username' | 'sgips'; @@ -59,6 +60,11 @@ export function LoginForm() { const [mfaCountdown, setMfaCountdown] = useState(0); const [mfaType, setMfaType] = useState<'sms' | 'email'>('sms'); // MFA 타입 + // 이메일 기반 인증 상태 + const [emailInput, setEmailInput] = useState(''); + const [showPasswordInput, setShowPasswordInput] = useState(false); + const [isWhitelistedEmail, setIsWhitelistedEmail] = useState(false); + // 일반 로그인 폼 데이터 const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); @@ -259,7 +265,7 @@ export function LoginForm() { } }; - // Email OTP 전송 + // Email OTP 전송/재전송 const handleSendEmail = async (userIdParam?: number, emailParam?: string, userNameParam?: string) => { const targetUserId = userIdParam || mfaUserId; const targetEmail = emailParam || mfaUserEmail; @@ -269,27 +275,19 @@ export function LoginForm() { setIsSmsLoading(true); try { - const response = await fetch('/api/auth/send-email-otp', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - userId: targetUserId, - email: targetEmail, - userName: targetUserName, - }), - }); + // 서버 액션 사용 + const result = await resendEmailOtp(targetUserId, targetEmail, targetUserName); - if (response.ok) { + if (result.success) { setMfaCountdown(60); toast({ title: '이메일 인증번호 전송', description: `${targetEmail}로 인증번호가 전송되었습니다.`, }); } else { - const errorData = await response.json(); toast({ title: t('errorTitle'), - description: errorData.error || '이메일 전송에 실패했습니다.', + description: result.error || '이메일 전송에 실패했습니다.', variant: 'destructive', }); } @@ -393,14 +391,85 @@ export function LoginForm() { } }; - // 일반 사용자명/패스워드 1차 인증 처리 - const handleUsernameLogin = async (e: React.FormEvent) => { + // 이메일 제출 핸들러 (1단계) + const handleEmailSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (!username || !password) { + if (!emailInput) { toast({ title: t('errorTitle'), - description: t('credentialsRequired'), + description: '이메일을 입력해주세요.', + variant: 'destructive', + }); + return; + } + + setIsFirstAuthLoading(true); + + try { + // 이메일 화이트리스트 확인 및 인증 시작 + const result = await checkEmailAndStartAuth(emailInput); + + if (!result.success) { + const errorMessage = getErrorMessage( + { errorCode: result.errorCode, message: result.error }, + 'email' + ); + + toast({ + title: t('errorTitle'), + description: errorMessage, + variant: 'destructive', + }); + return; + } + + if (result.isWhitelisted) { + // 화이트리스트 이메일: 바로 Email MFA로 이동 + setIsWhitelistedEmail(true); + setMfaUserId(result.userId!); + setMfaUserEmail(emailInput); + setMfaUserName(result.userName || ''); + setTempAuthKey(result.tempAuthKey!); + setMfaType('email'); + setShowMfaForm(true); + + toast({ + title: '이메일 인증', + description: `${emailInput}로 인증번호가 전송되었습니다.`, + }); + } else { + // 일반 이메일: 패스워드 입력 필요 + setIsWhitelistedEmail(false); + setUsername(emailInput); + setShowPasswordInput(true); + + toast({ + title: '패스워드 입력', + description: '패스워드를 입력해주세요.', + }); + } + } catch (error: unknown) { + console.error('Email submit error:', error); + + toast({ + title: t('errorTitle'), + description: '이메일 확인 중 오류가 발생했습니다.', + variant: 'destructive', + }); + } finally { + setIsFirstAuthLoading(false); + } + }; + + // 패스워드 제출 핸들러 (2단계 - 화이트리스트 아닌 경우) + const handlePasswordSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!password) { + toast({ + title: t('errorTitle'), + description: '패스워드를 입력해주세요.', variant: 'destructive', }); return; @@ -409,15 +478,14 @@ export function LoginForm() { setIsFirstAuthLoading(true); try { - // 1차 인증만 수행 (세션 생성 안함) + // 1차 인증 수행 (패스워드 검증) const authResult = await performFirstAuth(username, password, 'email'); if (authResult.success) { - const userMfaType = authResult.mfaType || 'sms'; - + // 패스워드 인증 성공 -> SMS MFA로 진행 toast({ title: t('firstAuthComplete'), - description: userMfaType === 'email' ? '이메일 인증을 진행합니다.' : t('proceedingSmsAuth'), + description: t('proceedingSmsAuth'), }); // MFA 화면으로 전환 @@ -425,27 +493,26 @@ export function LoginForm() { setMfaUserId(authResult.userId); setMfaUserEmail(authResult.email); setMfaUserName(authResult.userName || ''); - setMfaType(userMfaType); + setMfaType('sms'); setShowMfaForm(true); - // MFA 타입에 따라 자동으로 OTP 전송 + // SMS 자동 전송 setTimeout(() => { - if (userMfaType === 'email') { - handleSendEmail(authResult.userId, authResult.email, authResult.userName); - } else { - handleSendSms(authResult.userId); - } + handleSendSms(authResult.userId); }, 500); toast({ - title: userMfaType === 'email' ? '이메일 인증 필요' : t('smsAuthRequired'), - description: userMfaType === 'email' ? '이메일로 인증번호를 전송하고 있습니다.' : t('sendingCodeToPhone'), + title: t('smsAuthRequired'), + description: t('sendingCodeToPhone'), }); } } catch (error: unknown) { - console.error('Username login error:', error); - - const errorMessage = getErrorMessage(error as { errorCode?: string; message?: string }, 'email'); + console.error('Password submit error:', error); + + const errorMessage = getErrorMessage( + error as { errorCode?: string; message?: string }, + 'email' + ); toast({ title: t('errorTitle'), @@ -576,16 +643,41 @@ export function LoginForm() { } }; - // MFA 화면에서 뒤로 가기 + // MFA 화면 또는 패스워드 입력에서 뒤로 가기 const handleBackToLogin = () => { - setShowMfaForm(false); - setMfaToken(''); - setTempAuthKey(''); - setMfaUserId(null); - setMfaUserEmail(''); - setMfaUserName(''); - setMfaType('sms'); // 기본값으로 초기화 - setMfaCountdown(0); + // MFA 화면인 경우 + if (showMfaForm) { + // 화이트리스트 이메일인 경우 이메일 입력으로 돌아감 + if (isWhitelistedEmail) { + setShowMfaForm(false); + setMfaToken(''); + setTempAuthKey(''); + setMfaUserId(null); + setMfaUserEmail(''); + setMfaUserName(''); + setMfaType('email'); + setMfaCountdown(0); + setIsWhitelistedEmail(false); + } else { + // 일반 이메일인 경우 패스워드 입력으로 돌아감 + setShowMfaForm(false); + setMfaToken(''); + setTempAuthKey(''); + setMfaUserId(null); + setMfaUserEmail(''); + setMfaUserName(''); + setMfaType('sms'); + setMfaCountdown(0); + setShowPasswordInput(true); + } + } else if (showPasswordInput) { + // 패스워드 입력 화면인 경우 이메일 입력으로 돌아감 + setShowPasswordInput(false); + setPassword(''); + setUsername(''); + } + + // S-Gips 관련 초기화 setSelectedOtpUser(null); setShowUserSelectionDialog(false); setOtpUsers([]); @@ -699,40 +791,82 @@ export function LoginForm() { {/* Username Login Form */} {loginMethod === 'username' && ( - <form onSubmit={handleUsernameLogin} className="grid gap-4"> - <div className="grid gap-2"> - <Input - id="username" - type="text" - placeholder={t('emailPlaceholder')} - required - className="h-10" - value={username} - onChange={(e) => setUsername(e.target.value)} - disabled={isFirstAuthLoading} - /> - </div> - <div className="grid gap-2"> - <Input - id="password" - type="password" - placeholder={t('password')} - required - className="h-10" - value={password} - onChange={(e) => setPassword(e.target.value)} - disabled={isFirstAuthLoading} - /> - </div> - <Button - type="submit" - className="w-full" - variant="samsung" - disabled={isFirstAuthLoading || !username || !password} - > - {isFirstAuthLoading ? t('authenticating') : t('login')} - </Button> - </form> + <> + {!showPasswordInput ? ( + // 1단계: 이메일만 입력 + <form onSubmit={handleEmailSubmit} className="grid gap-4"> + <div className="grid gap-2"> + <Input + id="email" + type="email" + placeholder={t('emailPlaceholder')} + required + className="h-10" + value={emailInput} + onChange={(e) => setEmailInput(e.target.value)} + disabled={isFirstAuthLoading} + autoFocus + /> + </div> + <Button + type="submit" + className="w-full" + variant="samsung" + disabled={isFirstAuthLoading || !emailInput} + > + {isFirstAuthLoading ? t('authenticating') : '다음'} + </Button> + </form> + ) : ( + // 2단계: 패스워드 입력 + <div className="space-y-4"> + {/* 뒤로 가기 버튼 */} + <div className="flex items-center justify-start"> + <Button + type="button" + variant="ghost" + size="sm" + onClick={handleBackToLogin} + className="text-blue-600 hover:text-blue-800" + > + <ArrowLeft className="w-4 h-4 mr-1" /> + {t('backToLogin')} + </Button> + </div> + + {/* 이메일 표시 */} + <div className="bg-gray-50 p-3 rounded-lg"> + <p className="text-sm text-gray-600 mb-1">로그인 이메일</p> + <p className="text-sm font-medium text-gray-900">{emailInput}</p> + </div> + + {/* 패스워드 입력 폼 */} + <form onSubmit={handlePasswordSubmit} className="grid gap-4"> + <div className="grid gap-2"> + <Input + id="password" + type="password" + placeholder={t('password')} + required + className="h-10" + value={password} + onChange={(e) => setPassword(e.target.value)} + disabled={isFirstAuthLoading} + autoFocus + /> + </div> + <Button + type="submit" + className="w-full" + variant="samsung" + disabled={isFirstAuthLoading || !password} + > + {isFirstAuthLoading ? t('authenticating') : t('login')} + </Button> + </form> + </div> + )} + </> )} {/* S-Gips Login Form - 영문 페이지에서 비활성화 0925 구매 요청사항*/} @@ -778,6 +912,7 @@ export function LoginForm() { {/* Additional Links */} <div className="flex flex-col gap-2 text-center"> + {/* 신규 업체 등록은 항상 표시 */} <Button type="button" variant="link" @@ -787,7 +922,8 @@ export function LoginForm() { {t('newVendor')} </Button> - {loginMethod === 'username' && ( + {/* 비밀번호 찾기는 패스워드 입력 단계에서만 표시 */} + {loginMethod === 'username' && showPasswordInput && ( <Button type="button" variant="link" |
