'use client'; import { useState, useEffect } from "react"; import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Ship, InfoIcon, GlobeIcon, ChevronDownIcon, ArrowLeft } from "lucide-react"; import { useToast } from "@/hooks/use-toast"; import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem } from "@/components/ui/dropdown-menu" import { useTranslation } from '@/i18n/client' import { useRouter, useParams, usePathname, useSearchParams } from 'next/navigation'; import { signIn, useSession } from 'next-auth/react'; import { buttonVariants } from "@/components/ui/button" import Link from "next/link" import { useFormState } from 'react-dom'; import { InputOTP, InputOTPGroup, 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"; import { VideoBackground } from "./video-background"; export function LoginForm() { const params = useParams() || {}; const pathname = usePathname() || ''; const router = useRouter(); const searchParams = useSearchParams(); const lng = params.lng as string; const { t, i18n } = useTranslation(lng, 'login'); const { toast } = useToast(); const { data: session, status } = useSession(); // 상태 관리 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(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 [emailInput, setEmailInput] = useState(''); const [showPasswordInput, setShowPasswordInput] = useState(false); const [isWhitelistedEmail, setIsWhitelistedEmail] = useState(false); // 일반 로그인 폼 데이터 const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [isMfaLoading, setIsMfaLoading] = useState(false); const [isSmsLoading, setIsSmsLoading] = useState(false); // 서버 액션 상태 const [passwordResetState, passwordResetAction] = useFormState(requestPasswordResetAction, { success: false, error: undefined, message: undefined, }); // 호스트명 확인 상태 추가 const [isDataRoomHost, setIsDataRoomHost] = useState(false); // 컴포넌트 마운트 시 호스트명 확인 useEffect(() => { if (typeof window !== 'undefined') { const hostname = window.location.hostname; setIsDataRoomHost(hostname.includes('shidataroom')); } }, []); // // 영문 페이지에서 S-Gips 로그인 비활성화 시 기본 로그인 방법 설정 // useEffect(() => { // if (lng === 'en' && loginMethod === 'sgips') { // setLoginMethod('username'); // } // }, [lng, loginMethod]); // 이미 로그인된 사용자 리다이렉트 처리 useEffect(() => { if (status === 'authenticated' && session?.user) { const callbackUrlParam = searchParams?.get('callbackUrl'); if (callbackUrlParam) { try { // URL 객체로 파싱 const callbackUrl = new URL(callbackUrlParam); // pathname + search만 사용 (호스트 제거) const relativeUrl = callbackUrl.pathname + callbackUrl.search; router.push(relativeUrl); } catch { // 유효하지 않은 URL이면 그대로 사용 (이미 상대 경로일 수 있음) router.push(callbackUrlParam); } } else { if (session.user.ownerCompanyId) { router.push(`/${lng}/partners/data-room`); } else { router.push(`/${lng}/partners/dashboard`); } } } }, [status, session, router, lng, searchParams]); const handleChangeLanguage = (lang: string) => { const segments = pathname.split('/'); segments[1] = lang; router.push(segments.join('/')); }; const currentLanguageText = i18n.language === 'ko' ? t('languages.korean') : t('languages.english'); // 세분화된 에러 메시지 처리 함수 const getErrorMessage = (error: { errorCode?: string; message?: string }) => { const errorCode = error.errorCode; if (!errorCode) { return error.message || t('authenticationFailed'); } switch (errorCode) { case 'INVALID_CREDENTIALS': return t('invalidCredentials'); case 'ACCOUNT_LOCKED': return t('accountLocked'); case 'ACCOUNT_DEACTIVATED': return t('accountDeactivated'); case 'RATE_LIMITED': return t('rateLimited'); case 'VENDOR_NOT_FOUND': return t('vendorNotFound'); case 'SYSTEM_ERROR': return t('systemError'); default: return error.message || t('authenticationFailed'); } }; const goToVendorRegistration = () => { router.push(`/${lng}/partners/repository`); }; // MFA 카운트다운 효과 useEffect(() => { if (mfaCountdown > 0) { const timer = setTimeout(() => setMfaCountdown(mfaCountdown - 1), 1000); return () => clearTimeout(timer); } }, [mfaCountdown]); // 서버 액션 결과 처리 useEffect(() => { if (passwordResetState.success && passwordResetState.message) { toast({ title: t('resetLinkSent'), description: passwordResetState.message, }); setShowForgotPassword(false); } else if (passwordResetState.error) { toast({ title: t('errorTitle'), description: passwordResetState.error, variant: 'destructive', }); } }, [passwordResetState, toast, t]); // 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) { // 세분화된 에러 메시지 처리 const error = new Error(result.error || t('authenticationFailed')) as Error & { errorCode?: string }; error.errorCode = result.errorCode; throw error; } return result; } catch (error) { console.error('First auth error:', error); throw error; } }; // SMS 토큰 전송 const handleSendSms = async (userIdParam?: number) => { const targetUserId = userIdParam || mfaUserId; if (!targetUserId || mfaCountdown > 0) return; setIsSmsLoading(true); try { const requestBody: { userId: number } = { userId: targetUserId }; const response = await fetch('/api/auth/send-sms', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody), }); if (response.ok) { setMfaCountdown(60); toast({ title: t('smsSent'), description: t('smsCodeSent'), }); } else { const errorData = await response.json(); toast({ title: t('errorTitle'), description: errorData.message || t('smsFailure'), variant: 'destructive', }); } } catch (error) { console.error('SMS send error:', error); toast({ title: t('errorTitle'), description: t('smsError'), variant: 'destructive', }); } finally { setIsSmsLoading(false); } }; // 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 result = await resendEmailOtp(targetUserId, targetEmail, targetUserName); if (result.success) { setMfaCountdown(60); toast({ title: t('emailOtpSentTitle'), description: t('emailOtpSentTo', { email: targetEmail }), }); } else { toast({ title: t('errorTitle'), description: result.error || t('emailOtpSendFailed'), variant: 'destructive', }); } } catch (error) { console.error('Email OTP send error:', error); toast({ title: t('errorTitle'), description: t('emailOtpSendError'), variant: 'destructive', }); } finally { setIsSmsLoading(false); } }; // MFA 토큰 검증 및 최종 로그인 const handleMfaSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!mfaToken || mfaToken.length !== 6) { toast({ title: t('errorTitle'), description: t('enterSixDigitCode'), variant: 'destructive', }); return; } if (!tempAuthKey) { toast({ title: t('errorTitle'), description: t('authSessionExpired'), variant: 'destructive', }); setShowMfaForm(false); return; } setIsMfaLoading(true); try { // NextAuth의 credentials-mfa 프로바이더로 최종 인증 (mfaType 포함) const result = await signIn('credentials-mfa', { userId: mfaUserId, smsToken: mfaToken, tempAuthKey: tempAuthKey, mfaType: mfaType, // 'sms' 또는 'email' redirect: false, }); if (result?.ok) { toast({ title: t('authenticationComplete'), description: t('loginCompleted'), }); // 콜백 URL 처리 const callbackUrlParam = searchParams?.get('callbackUrl'); if (callbackUrlParam) { try { const callbackUrl = new URL(callbackUrlParam); const relativeUrl = callbackUrl.pathname + callbackUrl.search; router.push(relativeUrl); } catch { router.push(callbackUrlParam); } } else { router.push(`/${lng}/partners/dashboard`); } } else { let errorMessage = t('invalidAuthCode'); if (result?.error) { switch (result.error) { case 'CredentialsSignin': errorMessage = t('authCodeExpired'); break; case 'AccessDenied': errorMessage = t('accessDenied'); break; default: errorMessage = t('mfaAuthFailed'); } } toast({ title: t('errorTitle'), description: errorMessage, variant: 'destructive', }); } } catch (error) { console.error('MFA verification error:', error); toast({ title: t('errorTitle'), description: t('mfaAuthError'), variant: 'destructive', }); } finally { setIsMfaLoading(false); } }; // 이메일 제출 핸들러 (1단계) const handleEmailSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!emailInput) { toast({ title: t('errorTitle'), description: t('pleaseEnterEmail'), variant: 'destructive', }); return; } setIsFirstAuthLoading(true); try { // 이메일 화이트리스트 확인 및 인증 시작 const result = await checkEmailAndStartAuth(emailInput); if (!result.success) { const errorMessage = getErrorMessage( { errorCode: result.errorCode, message: result.error } ); 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: t('emailVerification'), description: t('emailOtpSentTo', { email: emailInput }), }); } else { // 일반 이메일: 패스워드 입력 필요 setIsWhitelistedEmail(false); setUsername(emailInput); setShowPasswordInput(true); toast({ title: t('passwordInput'), description: t('pleaseEnterPassword'), }); } } catch (error: unknown) { console.error('Email submit error:', error); toast({ title: t('errorTitle'), description: t('emailVerificationError'), variant: 'destructive', }); } finally { setIsFirstAuthLoading(false); } }; // 패스워드 제출 핸들러 (2단계 - 화이트리스트 아닌 경우) const handlePasswordSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!password) { toast({ title: t('errorTitle'), description: t('pleaseEnterPassword'), variant: 'destructive', }); return; } setIsFirstAuthLoading(true); try { // 1차 인증 수행 (패스워드 검증) const authResult = await performFirstAuth(username, password, 'email'); if (authResult.success) { // 패스워드 인증 성공 -> SMS MFA로 진행 toast({ title: t('firstAuthComplete'), description: t('proceedingSmsAuth'), }); // MFA 화면으로 전환 setTempAuthKey(authResult.tempAuthKey); setMfaUserId(authResult.userId); setMfaUserEmail(authResult.email); setMfaUserName(authResult.userName || ''); setMfaType('sms'); setShowMfaForm(true); // SMS 자동 전송 setTimeout(() => { handleSendSms(authResult.userId); }, 500); toast({ title: t('smsAuthRequired'), description: t('sendingCodeToPhone'), }); } } catch (error: unknown) { console.error('Password submit error:', error); const errorMessage = getErrorMessage( error as { errorCode?: string; message?: string } ); toast({ title: t('errorTitle'), description: errorMessage, variant: 'destructive', }); } finally { setIsFirstAuthLoading(false); } }; // MFA 화면 또는 패스워드 입력에서 뒤로 가기 const handleBackToLogin = () => { // 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(''); } }; // 세션 로딩 중이거나 이미 인증된 상태에서는 로딩 표시 if (status === 'loading') { return ( ); } // 이미 인증된 상태에서는 빈 화면 (리다이렉트 중) if (status === 'authenticated' && session?.user) { return ( ); } return (
{/* Left Content */}
{/* Top bar with Logo + eVCP (left) and "Request Vendor Repository" (right) */}
{isDataRoomHost ? "Data Room" : "eVCP"}
{!isDataRoomHost && ( {t('registerVendor')} )}
{/* Content section that occupies remaining space, centered vertically */}
{/* Header */}
{!showMfaForm ? ( <>

{isDataRoomHost ? t('loginMessageDataRoom') :t('loginMessage')}

{isDataRoomHost?t('loginDescriptionDataRoom') :t('loginDescription')}

) : ( <>
{mfaType === 'email' ? '📧' : '🔐'}

{mfaType === 'email' ? t('emailVerification') : t('smsVerification')}

{mfaType === 'email' ? t('emailOtpSentTo', { email: mfaUserEmail }) : t('firstAuthCompleteFor', { email: mfaUserEmail }) }

{mfaType === 'email' ? t('enterSixDigitEmailCode') : t('enterSixDigitCodeInstructions')}

)}
{/* 로그인 폼 또는 MFA 폼 */} {!showMfaForm ? ( <> {/* Login Form */} <> {!showPasswordInput ? ( // 1단계: 이메일만 입력
setEmailInput(e.target.value)} disabled={isFirstAuthLoading} autoFocus />
) : ( // 2단계: 패스워드 입력
{/* 뒤로 가기 버튼 */}
{/* 이메일 표시 */}

{t('loginEmail')}

{emailInput}

{/* 패스워드 입력 폼 */}
setPassword(e.target.value)} disabled={isFirstAuthLoading} autoFocus />
)} {/* Additional Links */}
{/* 신규 업체 등록은 항상 표시 */} {!isDataRoomHost && ( )} {/* 비밀번호 찾기는 패스워드 입력 단계에서만 표시 */} {showPasswordInput && ( )}
) : ( /* MFA 입력 폼 */
{/* 뒤로 가기 버튼 */}
{/* OTP 재전송 섹션 (SMS/Email) */}

{t('resendCode')}

{mfaType === 'email' ? t('didNotReceiveEmail') : t('didNotReceiveCode')}

{/* SMS 토큰 입력 폼 */}
setMfaToken(value)} >
{/* 도움말 */}
⚠️

{t('didNotReceiveCode')}

  • {t('checkPhoneNumber')}
  • {t('checkSpamFolder')}
  • {t('useResendButton')}
)} {/* 비밀번호 재설정 다이얼로그 */} {showForgotPassword && !showMfaForm && (

{t('resetPassword')}

{t('resetDescription')}

)} {/* Language Selector - MFA 화면에서는 숨김 */} {!showMfaForm && (
handleChangeLanguage(value)} > {t('languages.english')} {t('languages.korean')}
)}
{/* Terms - MFA 화면에서는 숨김 */} {!showMfaForm && (
{t("agreement")}{" "} {t("privacyPolicy")}
)}
{/* Right BG 영상 영역 */}
) }