diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-07 01:43:36 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-07 01:43:36 +0000 |
| commit | fbb3b7f05737f9571b04b0a8f4f15c0928de8545 (patch) | |
| tree | 343247117a7587b8ef5c418c9528d1cf2e0b6f1c /components/login | |
| parent | 9945ad119686a4c3a66f7b57782750f78a366cfb (diff) | |
(대표님) 변경사항 20250707 10시 43분
Diffstat (limited to 'components/login')
| -rw-r--r-- | components/login/login-form-shi.tsx | 14 | ||||
| -rw-r--r-- | components/login/login-form.tsx | 287 |
2 files changed, 158 insertions, 143 deletions
diff --git a/components/login/login-form-shi.tsx b/components/login/login-form-shi.tsx index 6be8d5c8..862f9f8a 100644 --- a/components/login/login-form-shi.tsx +++ b/components/login/login-form-shi.tsx @@ -99,12 +99,12 @@ export function LoginFormSHI({ try { // next-auth의 Credentials Provider로 로그인 시도 - const result = await signIn('credentials', { + const result = await signIn('credentials-otp', { email, code: otp, redirect: false, // 커스텀 처리 위해 redirect: false }); - + if (result?.ok) { // 토스트 메시지 표시 toast({ @@ -204,9 +204,9 @@ export function LoginFormSHI({ <div className="mx-auto w-full flex flex-col space-y-6 sm:w-[350px]"> {/* Here's your existing login/OTP forms: */} - {!otpSent ? ( - // ( */} - <form onSubmit={handleSubmit} className="p-6 md:p-8"> + {/* {!otpSent ? ( */} + + <form onSubmit={handleOtpSubmit} className="p-6 md:p-8"> {/* <form onSubmit={handleOtpSubmit} className="p-6 md:p-8"> */} <div className="flex flex-col gap-6"> <div className="flex flex-col items-center text-center"> @@ -269,7 +269,7 @@ export function LoginFormSHI({ </div> </div> </form> - ) + {/* ) : ( @@ -323,7 +323,7 @@ export function LoginFormSHI({ </div> </div> </form> - )} + )} */} <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 hover:[&_a]:text-primary"> {t('termsMessage')} <a href="#">{t('termsOfService')}</a> {t('and')} diff --git a/components/login/login-form.tsx b/components/login/login-form.tsx index bb588ba0..a71fd15e 100644 --- a/components/login/login-form.tsx +++ b/components/login/login-form.tsx @@ -38,12 +38,13 @@ export function LoginForm({ // 상태 관리 const [loginMethod, setLoginMethod] = useState<LoginMethod>('username'); - const [isLoading, setIsLoading] = useState(false); + const [isFirstAuthLoading, setIsFirstAuthLoading] = useState(false); const [showForgotPassword, setShowForgotPassword] = useState(false); // MFA 관련 상태 const [showMfaForm, setShowMfaForm] = useState(false); const [mfaToken, setMfaToken] = useState(''); + const [tempAuthKey, setTempAuthKey] = useState(''); const [mfaUserId, setMfaUserId] = useState(''); const [mfaUserEmail, setMfaUserEmail] = useState(''); const [mfaCountdown, setMfaCountdown] = useState(0); @@ -56,6 +57,9 @@ export function LoginForm({ const [sgipsUsername, setSgipsUsername] = useState(''); const [sgipsPassword, setSgipsPassword] = useState(''); + const [isMfaLoading, setIsMfaLoading] = useState(false); + const [isSmsLoading, setIsSmsLoading] = useState(false); + // 서버 액션 상태 const [passwordResetState, passwordResetAction] = useFormState(requestPasswordResetAction, { success: false, @@ -100,29 +104,56 @@ export function LoginForm({ } }, [passwordResetState, toast, t]); - // SMS 토큰 전송 - const handleSendSms = async () => { - if (!mfaUserId || mfaCountdown > 0) return; + // 1차 인증 수행 (공통 함수) + const performFirstAuth = async (username: string, password: string, provider: 'email' | 'sgips') => { + try { + const response = await fetch('/api/auth/first-auth', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + username, + password, + provider + }), + }); + + const result = await response.json(); + + if (!response.ok) { + throw new Error(result.error || '인증에 실패했습니다.'); + } + + return result; + } catch (error) { + console.error('First auth error:', error); + throw error; + } + }; + + // SMS 토큰 전송 (userId 파라미터 추가) + const handleSendSms = async (userIdParam?: string) => { + const targetUserId = userIdParam || mfaUserId; + if (!targetUserId || mfaCountdown > 0) return; - setIsLoading(true); + setIsSmsLoading(true); try { - // SMS 전송 API 호출 (실제 구현 필요) const response = await fetch('/api/auth/send-sms', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ userId: mfaUserId }), + body: JSON.stringify({ userId: targetUserId }), }); if (response.ok) { - setMfaCountdown(60); // 60초 카운트다운 + setMfaCountdown(60); toast({ title: 'SMS 전송 완료', description: '인증번호를 전송했습니다.', }); } else { + const errorData = await response.json(); toast({ title: t('errorTitle'), - description: 'SMS 전송에 실패했습니다.', + description: errorData.message || 'SMS 전송에 실패했습니다.', variant: 'destructive', }); } @@ -134,11 +165,11 @@ export function LoginForm({ variant: 'destructive', }); } finally { - setIsLoading(false); + setIsSmsLoading(false); } }; - // MFA 토큰 검증 + // MFA 토큰 검증 및 최종 로그인 const handleMfaSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -151,26 +182,34 @@ export function LoginForm({ return; } - setIsLoading(true); + if (!tempAuthKey) { + toast({ + title: t('errorTitle'), + description: '인증 세션이 만료되었습니다. 다시 로그인해주세요.', + variant: 'destructive', + }); + setShowMfaForm(false); + return; + } + + setIsMfaLoading(true); try { - // MFA 토큰 검증 API 호출 - const response = await fetch('/api/auth/verify-mfa', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - userId: mfaUserId, - token: mfaToken - }), + // NextAuth의 credentials-mfa 프로바이더로 최종 인증 + const result = await signIn('credentials-mfa', { + userId: mfaUserId, + smsToken: mfaToken, + tempAuthKey: tempAuthKey, + redirect: false, }); - if (response.ok) { + if (result?.ok) { toast({ title: '인증 완료', description: '로그인이 완료되었습니다.', }); - // callbackUrl 처리 + // 콜백 URL 처리 const callbackUrlParam = searchParams?.get('callbackUrl'); if (callbackUrlParam) { try { @@ -184,10 +223,24 @@ export function LoginForm({ router.push(`/${lng}/partners/dashboard`); } } else { - const errorData = await response.json(); + let errorMessage = '인증번호가 올바르지 않습니다.'; + + if (result?.error) { + switch (result.error) { + case 'CredentialsSignin': + errorMessage = '인증번호가 올바르지 않거나 만료되었습니다.'; + break; + case 'AccessDenied': + errorMessage = '접근이 거부되었습니다.'; + break; + default: + errorMessage = 'MFA 인증에 실패했습니다.'; + } + } + toast({ title: t('errorTitle'), - description: errorData.message || '인증번호가 올바르지 않습니다.', + description: errorMessage, variant: 'destructive', }); } @@ -199,11 +252,11 @@ export function LoginForm({ variant: 'destructive', }); } finally { - setIsLoading(false); + setIsMfaLoading(false); } }; - // 일반 사용자명/패스워드 로그인 처리 (간소화된 버전) + // 일반 사용자명/패스워드 1차 인증 처리 const handleUsernameLogin = async (e: React.FormEvent) => { e.preventDefault(); @@ -216,76 +269,53 @@ export function LoginForm({ return; } - setIsLoading(true); + setIsFirstAuthLoading(true); try { - // NextAuth credentials-password provider로 로그인 - const result = await signIn('credentials-password', { - username: username, - password: password, - redirect: false, - }); + // 1차 인증만 수행 (세션 생성 안함) + const authResult = await performFirstAuth(username, password, 'email'); - if (result?.ok) { - // 로그인 1차 성공 - 바로 MFA 화면으로 전환 + if (authResult.success) { toast({ - title: t('loginSuccess'), - description: '1차 인증이 완료되었습니다.', + title: '1차 인증 완료', + description: 'SMS 인증을 진행합니다.', }); - // 모든 사용자는 MFA 필수이므로 바로 MFA 폼으로 전환 - setMfaUserId(username); // 입력받은 username 사용 - setMfaUserEmail(username); // 입력받은 username 사용 (보통 이메일) + // MFA 화면으로 전환 + setTempAuthKey(authResult.tempAuthKey); + setMfaUserId(authResult.userId); + setMfaUserEmail(authResult.email); setShowMfaForm(true); - // 자동으로 SMS 전송 + // 자동으로 SMS 전송 (userId 직접 전달) setTimeout(() => { - handleSendSms(); + handleSendSms(authResult.userId); }, 500); toast({ title: 'SMS 인증 필요', description: '등록된 전화번호로 인증번호를 전송합니다.', }); - - } else { - // 로그인 실패 처리 - let errorMessage = t('invalidCredentials'); - - if (result?.error) { - switch (result.error) { - case 'CredentialsSignin': - errorMessage = t('invalidCredentials'); - break; - case 'AccessDenied': - errorMessage = t('accessDenied'); - break; - default: - errorMessage = t('defaultErrorMessage'); - } - } - - toast({ - title: t('errorTitle'), - description: errorMessage, - variant: 'destructive', - }); } - } catch (error) { - console.error('S-GIPS Login error:', error); + } catch (error: any) { + console.error('Username login error:', error); + + let errorMessage = t('invalidCredentials'); + if (error.message) { + errorMessage = error.message; + } + toast({ title: t('errorTitle'), - description: t('defaultErrorMessage'), + description: errorMessage, variant: 'destructive', }); } finally { - setIsLoading(false); + setIsFirstAuthLoading(false); } }; - - // S-Gips 로그인 처리 - // S-Gips 로그인 처리 (간소화된 버전) + // S-Gips 1차 인증 처리 const handleSgipsLogin = async (e: React.FormEvent) => { e.preventDefault(); @@ -298,73 +328,62 @@ export function LoginForm({ return; } - setIsLoading(true); + setIsFirstAuthLoading(true); try { - // NextAuth credentials-password provider로 로그인 (S-Gips 구분) - const result = await signIn('credentials-password', { - username: sgipsUsername, - password: sgipsPassword, - provider: 'sgips', // S-Gips 구분을 위한 추가 파라미터 - redirect: false, - }); + // S-Gips 1차 인증만 수행 (세션 생성 안함) + const authResult = await performFirstAuth(sgipsUsername, sgipsPassword, 'sgips'); - if (result?.ok) { - // S-Gips 1차 인증 성공 - 바로 MFA 화면으로 전환 + if (authResult.success) { toast({ - title: t('loginSuccess'), - description: 'S-Gips 인증이 완료되었습니다.', + title: 'S-Gips 인증 완료', + description: 'SMS 인증을 진행합니다.', }); - // S-Gips도 MFA 필수이므로 바로 MFA 폼으로 전환 - setMfaUserId(sgipsUsername); - setMfaUserEmail(sgipsUsername); + // MFA 화면으로 전환 + setTempAuthKey(authResult.tempAuthKey); + setMfaUserId(authResult.userId); + setMfaUserEmail(authResult.email); setShowMfaForm(true); - // 자동으로 SMS 전송 + // 자동으로 SMS 전송 (userId 직접 전달) setTimeout(() => { - handleSendSms(); + handleSendSms(authResult.userId); }, 500); toast({ title: 'SMS 인증 시작', description: 'S-Gips 등록 전화번호로 인증번호를 전송합니다.', }); - - } else { - let errorMessage = t('sgipsLoginFailed'); - - if (result?.error) { - switch (result.error) { - case 'CredentialsSignin': - errorMessage = t('invalidSgipsCredentials'); - break; - case 'AccessDenied': - errorMessage = t('sgipsAccessDenied'); - break; - default: - errorMessage = t('sgipsSystemError'); - } - } - - toast({ - title: t('errorTitle'), - description: errorMessage, - variant: 'destructive', - }); } - } catch (error) { + } catch (error: any) { console.error('S-Gips login error:', error); + + let errorMessage = t('sgipsLoginFailed'); + if (error.message) { + errorMessage = error.message; + } + toast({ title: t('errorTitle'), - description: t('sgipsSystemError'), + description: errorMessage, variant: 'destructive', }); } finally { - setIsLoading(false); + setIsFirstAuthLoading(false); } }; + // MFA 화면에서 뒤로 가기 + const handleBackToLogin = () => { + setShowMfaForm(false); + setMfaToken(''); + setTempAuthKey(''); + setMfaUserId(''); + setMfaUserEmail(''); + setMfaCountdown(0); + }; + return ( <div className="container relative flex h-screen flex-col items-center justify-center md:grid lg:max-w-none lg:grid-cols-2 lg:px-0"> {/* Left Content */} @@ -405,7 +424,7 @@ export function LoginForm({ </div> <h1 className="text-2xl font-bold">SMS 인증</h1> <p className="text-sm text-muted-foreground mt-2"> - {mfaUserEmail}로 로그인하셨습니다 + {mfaUserEmail}로 1차 인증이 완료되었습니다 </p> <p className="text-xs text-muted-foreground mt-1"> 등록된 전화번호로 전송된 6자리 인증번호를 입력해주세요 @@ -457,7 +476,7 @@ export function LoginForm({ className="h-10" value={username} onChange={(e) => setUsername(e.target.value)} - disabled={isLoading} + disabled={isFirstAuthLoading} /> </div> <div className="grid gap-2"> @@ -469,16 +488,16 @@ export function LoginForm({ className="h-10" value={password} onChange={(e) => setPassword(e.target.value)} - disabled={isLoading} + disabled={isFirstAuthLoading} /> </div> <Button type="submit" className="w-full" variant="samsung" - disabled={isLoading || !username || !password} + disabled={isFirstAuthLoading || !username || !password} > - {isLoading ? '로그인 중...' : t('login')} + {isFirstAuthLoading ? '인증 중...' : t('login')} </Button> </form> )} @@ -495,7 +514,7 @@ export function LoginForm({ className="h-10" value={sgipsUsername} onChange={(e) => setSgipsUsername(e.target.value)} - disabled={isLoading} + disabled={isFirstAuthLoading} /> </div> <div className="grid gap-2"> @@ -507,16 +526,16 @@ export function LoginForm({ className="h-10" value={sgipsPassword} onChange={(e) => setSgipsPassword(e.target.value)} - disabled={isLoading} + disabled={isFirstAuthLoading} /> </div> <Button type="submit" className="w-full" variant="default" - disabled={isLoading || !sgipsUsername || !sgipsPassword} + disabled={isFirstAuthLoading || !sgipsUsername || !sgipsPassword} > - {isLoading ? 'S-Gips 인증 중...' : 'S-Gips 로그인'} + {isFirstAuthLoading ? 'S-Gips 인증 중...' : 'S-Gips 로그인'} </Button> <p className="text-xs text-muted-foreground text-center"> S-Gips 계정으로 로그인하면 자동으로 SMS 인증이 진행됩니다. @@ -553,6 +572,7 @@ export function LoginForm({ variant="link" className="text-green-600 hover:text-green-800 text-sm" onClick={() => { + setTempAuthKey('test-temp-key'); setMfaUserId('test-user'); setMfaUserEmail('test@example.com'); setShowMfaForm(true); @@ -572,13 +592,7 @@ export function LoginForm({ type="button" variant="ghost" size="sm" - onClick={() => { - setShowMfaForm(false); - setMfaToken(''); - setMfaUserId(''); - setMfaUserEmail(''); - setMfaCountdown(0); - }} + onClick={handleBackToLogin} className="text-blue-600 hover:text-blue-800" > <ArrowLeft className="w-4 h-4 mr-1" /> @@ -595,13 +609,14 @@ export function LoginForm({ 인증번호를 받지 못하셨나요? </p> <Button - onClick={handleSendSms} - disabled={isLoading || mfaCountdown > 0} + onClick={() => handleSendSms()} + disabled={isSmsLoading || mfaCountdown > 0} variant="outline" size="sm" className="w-full" + type="button" > - {isLoading ? ( + {isSmsLoading ? ( '전송 중...' ) : mfaCountdown > 0 ? ( `재전송 가능 (${mfaCountdown}초)` @@ -641,9 +656,9 @@ export function LoginForm({ type="submit" className="w-full" variant="samsung" - disabled={isLoading || mfaToken.length !== 6} + disabled={isMfaLoading || mfaToken.length !== 6} > - {isLoading ? '인증 중...' : '인증 완료'} + {isMfaLoading ? '인증 중...' : '인증 완료'} </Button> </form> @@ -755,7 +770,7 @@ export function LoginForm({ <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 hover:[&_a]:text-primary"> {t("agreement")}{" "} <Link - href={`/${lng}/privacy`} // 개인정보처리방침만 남김 + href={`/${lng}/privacy`} className="underline underline-offset-4 hover:text-primary" > {t("privacyPolicy")} |
