From a070f833d132e6370311c0bbdad03beb51d595df Mon Sep 17 00:00:00 2001
From: joonhoekim <26rote@gmail.com>
Date: Wed, 15 Oct 2025 21:38:21 +0900
Subject: (김준회) 이메일 화이트리스트 (SMS 우회) 기능 추가 및 기존 로그인 과정
 통합
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 components/login/login-form.tsx | 101 ++++++++++++++++++++++++++++++++++------
 1 file changed, 87 insertions(+), 14 deletions(-)

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

diff --git a/components/login/login-form.tsx b/components/login/login-form.tsx
index 090f3a70..d249b6b1 100644
--- a/components/login/login-form.tsx
+++ b/components/login/login-form.tsx
@@ -55,7 +55,9 @@ export function LoginForm() {
     const [tempAuthKey, setTempAuthKey] = useState('');
     const [mfaUserId, setMfaUserId] = useState<number | null>(null);
     const [mfaUserEmail, setMfaUserEmail] = useState('');
+    const [mfaUserName, setMfaUserName] = useState(''); // Email OTP 전송 시 필요
     const [mfaCountdown, setMfaCountdown] = useState(0);
+    const [mfaType, setMfaType] = useState<'sms' | 'email'>('sms'); // MFA 타입
 
     // 일반 로그인 폼 데이터
     const [username, setUsername] = useState('');
@@ -257,6 +259,52 @@ export function LoginForm() {
         }
     };
 
+    // Email OTP 전송
+    const handleSendEmail = async (userIdParam?: number, emailParam?: string, userNameParam?: string) => {
+        const targetUserId = userIdParam || mfaUserId;
+        const targetEmail = emailParam || mfaUserEmail;
+        const targetUserName = userNameParam || mfaUserName;
+        
+        if (!targetUserId || mfaCountdown > 0) return;
+
+        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,
+                }),
+            });
+
+            if (response.ok) {
+                setMfaCountdown(60);
+                toast({
+                    title: '이메일 인증번호 전송',
+                    description: `${targetEmail}로 인증번호가 전송되었습니다.`,
+                });
+            } else {
+                const errorData = await response.json();
+                toast({
+                    title: t('errorTitle'),
+                    description: errorData.error || '이메일 전송에 실패했습니다.',
+                    variant: 'destructive',
+                });
+            }
+        } catch (error) {
+            console.error('Email OTP send error:', error);
+            toast({
+                title: t('errorTitle'),
+                description: '이메일 전송 중 오류가 발생했습니다.',
+                variant: 'destructive',
+            });
+        } finally {
+            setIsSmsLoading(false);
+        }
+    };
+
     // MFA 토큰 검증 및 최종 로그인
     const handleMfaSubmit = async (e: React.FormEvent) => {
         e.preventDefault();
@@ -283,11 +331,12 @@ export function LoginForm() {
         setIsMfaLoading(true);
 
         try {
-            // NextAuth의 credentials-mfa 프로바이더로 최종 인증
+            // NextAuth의 credentials-mfa 프로바이더로 최종 인증 (mfaType 포함)
             const result = await signIn('credentials-mfa', {
                 userId: mfaUserId,
                 smsToken: mfaToken,
                 tempAuthKey: tempAuthKey,
+                mfaType: mfaType, // 'sms' 또는 'email'
                 redirect: false,
             });
 
@@ -364,25 +413,33 @@ export function LoginForm() {
             const authResult = await performFirstAuth(username, password, 'email');
 
             if (authResult.success) {
+                const userMfaType = authResult.mfaType || 'sms';
+                
                 toast({
                     title: t('firstAuthComplete'),
-                    description: t('proceedingSmsAuth'),
+                    description: userMfaType === 'email' ? '이메일 인증을 진행합니다.' : t('proceedingSmsAuth'),
                 });
 
                 // MFA 화면으로 전환
                 setTempAuthKey(authResult.tempAuthKey);
                 setMfaUserId(authResult.userId);
                 setMfaUserEmail(authResult.email);
+                setMfaUserName(authResult.userName || '');
+                setMfaType(userMfaType);
                 setShowMfaForm(true);
 
-                // 자동으로 SMS 전송 (userId 직접 전달)
+                // MFA 타입에 따라 자동으로 OTP 전송
                 setTimeout(() => {
-                    handleSendSms(authResult.userId);
+                    if (userMfaType === 'email') {
+                        handleSendEmail(authResult.userId, authResult.email, authResult.userName);
+                    } else {
+                        handleSendSms(authResult.userId);
+                    }
                 }, 500);
 
                 toast({
-                    title: t('smsAuthRequired'),
-                    description: t('sendingCodeToPhone'),
+                    title: userMfaType === 'email' ? '이메일 인증 필요' : t('smsAuthRequired'),
+                    description: userMfaType === 'email' ? '이메일로 인증번호를 전송하고 있습니다.' : t('sendingCodeToPhone'),
                 });
             }
         } catch (error: unknown) {
@@ -526,6 +583,8 @@ export function LoginForm() {
         setTempAuthKey('');
         setMfaUserId(null);
         setMfaUserEmail('');
+        setMfaUserName('');
+        setMfaType('sms'); // 기본값으로 초기화
         setMfaCountdown(0);
         setSelectedOtpUser(null);
         setShowUserSelectionDialog(false);
@@ -582,17 +641,23 @@ export function LoginForm() {
                                     ) : (
                                         <>
                                             <div className="flex items-center justify-center w-12 h-12 rounded-full bg-blue-100 mb-4">
-                                                🔐
+                                                {mfaType === 'email' ? '📧' : '🔐'}
                                             </div>
-                                            <h1 className="text-2xl font-bold">{t('smsVerification')}</h1>
+                                            <h1 className="text-2xl font-bold">
+                                                {mfaType === 'email' ? '이메일 인증' : t('smsVerification')}
+                                            </h1>
                                             <p className="text-sm text-muted-foreground mt-2">
                                                 {selectedOtpUser
                                                     ? t('firstAuthCompleteForSgips', { name: selectedOtpUser.name, email: mfaUserEmail })
-                                                    : t('firstAuthCompleteFor', { email: mfaUserEmail })
+                                                    : mfaType === 'email'
+                                                        ? `${mfaUserEmail}로 인증번호가 전송되었습니다.`
+                                                        : t('firstAuthCompleteFor', { email: mfaUserEmail })
                                                 }
                                             </p>
                                             <p className="text-xs text-muted-foreground mt-1">
-                                                {t('enterSixDigitCodeInstructions')}
+                                                {mfaType === 'email' 
+                                                    ? '이메일에서 받은 6자리 인증번호를 입력해주세요.' 
+                                                    : t('enterSixDigitCodeInstructions')}
                                             </p>
                                         </>
                                     )}
@@ -751,16 +816,24 @@ export function LoginForm() {
                                             </Button>
                                         </div>
 
-                                        {/* SMS 재전송 섹션 */}
+                                        {/* OTP 재전송 섹션 (SMS/Email) */}
                                         <div className="bg-gray-50 p-4 rounded-lg">
                                             <h3 className="text-sm font-medium text-gray-900 mb-2">
                                                 {t('resendCode')}
                                             </h3>
                                             <p className="text-xs text-gray-600 mb-3">
-                                                {t('didNotReceiveCode')}
+                                                {mfaType === 'email' 
+                                                    ? '이메일을 받지 못하셨나요?' 
+                                                    : t('didNotReceiveCode')}
                                             </p>
                                             <Button
-                                                onClick={() => handleSendSms()}
+                                                onClick={() => {
+                                                    if (mfaType === 'email') {
+                                                        handleSendEmail();
+                                                    } else {
+                                                        handleSendSms();
+                                                    }
+                                                }}
                                                 disabled={isSmsLoading || mfaCountdown > 0}
                                                 variant="outline"
                                                 size="sm"
@@ -772,7 +845,7 @@ export function LoginForm() {
                                                 ) : mfaCountdown > 0 ? (
                                                     t('resendAvailable', { seconds: mfaCountdown })
                                                 ) : (
-                                                    t('resendCode')
+                                                    mfaType === 'email' ? '인증번호 재전송' : t('resendCode')
                                                 )}
                                             </Button>
                                         </div>
-- 
cgit v1.2.3

