'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 } from 'next-auth/react'; import { buttonVariants } from "@/components/ui/button" import Link from "next/link" import Image from 'next/image'; import { useFormState } from 'react-dom'; import { InputOTP, InputOTPGroup, InputOTPSlot, } from "@/components/ui/input-otp" import { requestPasswordResetAction } from "@/lib/users/auth/partners-auth"; type LoginMethod = 'username' | 'sgips'; 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 [loginMethod, setLoginMethod] = useState('username'); 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 [mfaCountdown, setMfaCountdown] = useState(0); // 일반 로그인 폼 데이터 const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); // S-Gips 로그인 폼 데이터 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, error: undefined, message: undefined, }); 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 }, provider: 'email' | 'sgips') => { const errorCode = error.errorCode; if (!errorCode) { return error.message || t('authenticationFailed'); } switch (errorCode) { case 'INVALID_CREDENTIALS': return provider === 'sgips' ? t('sgipsInvalidCredentials') : 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 토큰 전송 (userId 파라미터 추가) const handleSendSms = async (userIdParam?: number) => { const targetUserId = userIdParam || mfaUserId; if (!targetUserId || mfaCountdown > 0) return; setIsSmsLoading(true); try { const response = await fetch('/api/auth/send-sms', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId: targetUserId }), }); 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); } }; // 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 프로바이더로 최종 인증 const result = await signIn('credentials-mfa', { userId: mfaUserId, smsToken: mfaToken, tempAuthKey: tempAuthKey, 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 handleUsernameLogin = async (e: React.FormEvent) => { e.preventDefault(); if (!username || !password) { toast({ title: t('errorTitle'), description: t('credentialsRequired'), variant: 'destructive', }); return; } setIsFirstAuthLoading(true); try { // 1차 인증만 수행 (세션 생성 안함) const authResult = await performFirstAuth(username, password, 'email'); if (authResult.success) { toast({ title: t('firstAuthComplete'), description: t('proceedingSmsAuth'), }); // MFA 화면으로 전환 setTempAuthKey(authResult.tempAuthKey); setMfaUserId(authResult.userId); setMfaUserEmail(authResult.email); setShowMfaForm(true); // 자동으로 SMS 전송 (userId 직접 전달) setTimeout(() => { handleSendSms(authResult.userId); }, 500); toast({ 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'); toast({ title: t('errorTitle'), description: errorMessage, variant: 'destructive', }); } finally { setIsFirstAuthLoading(false); } }; // S-Gips 1차 인증 처리 const handleSgipsLogin = async (e: React.FormEvent) => { e.preventDefault(); if (!sgipsUsername || !sgipsPassword) { toast({ title: t('errorTitle'), description: t('credentialsRequired'), variant: 'destructive', }); return; } setIsFirstAuthLoading(true); try { // S-Gips 1차 인증만 수행 (세션 생성 안함) const authResult = await performFirstAuth(sgipsUsername, sgipsPassword, 'sgips'); if (authResult.success) { toast({ title: t('sgipsAuthComplete'), description: t('proceedingSmsAuth'), }); // MFA 화면으로 전환 setTempAuthKey(authResult.tempAuthKey); setMfaUserId(authResult.userId); setMfaUserEmail(authResult.email); setShowMfaForm(true); // 자동으로 SMS 전송 (userId 직접 전달) setTimeout(() => { handleSendSms(authResult.userId); }, 500); toast({ title: t('smsAuthStarted'), description: t('sendingCodeToSgipsPhone'), }); } } catch (error: unknown) { console.error('S-Gips login error:', error); const errorMessage = getErrorMessage(error as { errorCode?: string; message?: string }, 'sgips'); toast({ title: t('errorTitle'), description: errorMessage, variant: 'destructive', }); } finally { setIsFirstAuthLoading(false); } }; // MFA 화면에서 뒤로 가기 const handleBackToLogin = () => { setShowMfaForm(false); setMfaToken(''); setTempAuthKey(''); setMfaUserId(null); setMfaUserEmail(''); setMfaCountdown(0); }; return (
{/* Left Content */}
{/* Top bar with Logo + eVCP (left) and "Request Vendor Repository" (right) */}
eVCP
{t('registerVendor')}
{/* Content section that occupies remaining space, centered vertically */}
{/* Header */}
{!showMfaForm ? ( <>

{t('loginMessage')}

{t('loginDescription')}

) : ( <>
🔐

{t('smsVerification')}

{t('firstAuthCompleteFor', { email: mfaUserEmail })}

{t('enterSixDigitCodeInstructions')}

)}
{/* 로그인 폼 또는 MFA 폼 */} {!showMfaForm ? ( <> {/* Login Method Tabs */}
{/* Username Login Form */} {loginMethod === 'username' && (
setUsername(e.target.value)} disabled={isFirstAuthLoading} />
setPassword(e.target.value)} disabled={isFirstAuthLoading} />
)} {/* S-Gips Login Form */} {loginMethod === 'sgips' && (
setSgipsUsername(e.target.value)} disabled={isFirstAuthLoading} />
setSgipsPassword(e.target.value)} disabled={isFirstAuthLoading} />

{t('sgipsAutoSms')}

)} {/* Additional Links */}
{loginMethod === 'username' && ( )}
) : ( /* MFA 입력 폼 */
{/* 뒤로 가기 버튼 */}
{/* SMS 재전송 섹션 */}

{t('resendCode')}

{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 이미지 영역 */}
Background image

“{t("blockquote")}”

) }