summaryrefslogtreecommitdiff
path: root/components/login
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-07-07 01:43:36 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-07-07 01:43:36 +0000
commitfbb3b7f05737f9571b04b0a8f4f15c0928de8545 (patch)
tree343247117a7587b8ef5c418c9528d1cf2e0b6f1c /components/login
parent9945ad119686a4c3a66f7b57782750f78a366cfb (diff)
(대표님) 변경사항 20250707 10시 43분
Diffstat (limited to 'components/login')
-rw-r--r--components/login/login-form-shi.tsx14
-rw-r--r--components/login/login-form.tsx287
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")}