From c49d5a42a66d1d29d477cca2ad56f923313c3961 Mon Sep 17 00:00:00 2001
From: joonhoekim <26rote@gmail.com>
Date: Thu, 16 Oct 2025 18:11:48 +0900
Subject: (김준회) 파트너 로그인 시, 이메일 화이트리스트 대상은 비밀번호 입력
 안하도록 변경
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 components/login/login-form.tsx | 290 +++++++++++++++++++++++++++++-----------
 1 file changed, 213 insertions(+), 77 deletions(-)

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

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

