From fbb3b7f05737f9571b04b0a8f4f15c0928de8545 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Mon, 7 Jul 2025 01:43:36 +0000
Subject: (대표님) 변경사항 20250707 10시 43분
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 components/login/login-form.tsx | 287 +++++++++++++++++++++-------------------
 1 file changed, 151 insertions(+), 136 deletions(-)

(limited to 'components/login/login-form.tsx')

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")}
-- 
cgit v1.2.3

