'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 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"; import Loading from "../common/loading/loading"; type LoginMethod = 'username' | 'sgips'; type OtpUser = { name: string; vndrcd: string; phone: string; email: string; nation_cd: string; userId: number; // 백엔드에서 생성된 로컬 DB 사용자 ID }; 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 [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 [mfaUserName, setMfaUserName] = useState(''); // Email OTP 전송 시 필요 const [mfaCountdown, setMfaCountdown] = useState(0); const [mfaType, setMfaType] = useState<'sms' | 'email'>('sms'); // MFA 타입 // 일반 로그인 폼 데이터 const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); // S-Gips 로그인 폼 데이터 const [sgipsUsername, setSgipsUsername] = useState(''); const [sgipsPassword, setSgipsPassword] = useState(''); // OTP 사용자 선택 관련 상태 const [otpUsers, setOtpUsers] = useState([]); const [showUserSelectionDialog, setShowUserSelectionDialog] = useState(false); const [selectedOtpUser, setSelectedOtpUser] = useState(null); const [isMfaLoading, setIsMfaLoading] = useState(false); const [isSmsLoading, setIsSmsLoading] = useState(false); // 서버 액션 상태 const [passwordResetState, passwordResetAction] = useFormState(requestPasswordResetAction, { success: false, error: undefined, message: undefined, }); // // 영문 페이지에서 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 { // callbackUrl이 없으면 기본 대시보드로 리다이렉트 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 }, 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 토큰 전송 const handleSendSms = async (userIdParam?: number) => { const targetUserId = userIdParam || mfaUserId; if (!targetUserId || mfaCountdown > 0) return; setIsSmsLoading(true); try { const requestBody: { userId: number; phone?: string; name?: string } = { userId: targetUserId }; // S-GIPS 사용자인 경우 추가 정보 포함 if (selectedOtpUser) { requestBody.phone = selectedOtpUser.phone; requestBody.name = selectedOtpUser.name; } const response = await fetch('/api/auth/send-sms', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody), }); if (response.ok) { setMfaCountdown(60); if (selectedOtpUser) { toast({ title: t('smsSent'), description: t('smsCodeSentTo', { name: selectedOtpUser.name, phone: selectedOtpUser.phone }), }); } else { 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 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(); 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 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) { const userMfaType = authResult.mfaType || 'sms'; toast({ title: t('firstAuthComplete'), description: userMfaType === 'email' ? '이메일 인증을 진행합니다.' : t('proceedingSmsAuth'), }); // MFA 화면으로 전환 setTempAuthKey(authResult.tempAuthKey); setMfaUserId(authResult.userId); setMfaUserEmail(authResult.email); setMfaUserName(authResult.userName || ''); setMfaType(userMfaType); setShowMfaForm(true); // MFA 타입에 따라 자동으로 OTP 전송 setTimeout(() => { if (userMfaType === 'email') { handleSendEmail(authResult.userId, authResult.email, authResult.userName); } else { handleSendSms(authResult.userId); } }, 500); toast({ title: userMfaType === 'email' ? '이메일 인증 필요' : t('smsAuthRequired'), description: userMfaType === 'email' ? '이메일로 인증번호를 전송하고 있습니다.' : 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) { const users = authResult.otpUsers || []; if (users.length === 0) { toast({ title: t('errorTitle'), description: t('noUsersFound'), variant: 'destructive', }); return; } // 사용자 선택 다이얼로그 표시 (항상) setOtpUsers(users); setShowUserSelectionDialog(true); } } 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); } }; // 선택된 OTP 사용자와 함께 MFA 진행 const proceedWithSelectedUser = async (user: OtpUser, tempAuthKey: string) => { try { // 사용자 정보를 기반으로 MFA 진행 setTempAuthKey(tempAuthKey); setSelectedOtpUser(user); setMfaUserId(user.userId); // 선택된 사용자의 userId 설정 setMfaUserEmail(user.email); setShowMfaForm(true); // 선택된 사용자의 userId를 직접 전달하여 SMS 전송 setTimeout(() => { handleSendSms(user.userId); }, 2000); toast({ title: t('sgipsAuthComplete'), description: t('sendingCodeToSelectedUser', { name: user.name }), }); } catch (error) { console.error('Proceeding with selected user error:', error); toast({ title: t('errorTitle'), description: t('mfaSetupError'), variant: 'destructive', }); } }; // OTP 사용자 선택 처리 const handleUserSelection = async (user: OtpUser) => { setShowUserSelectionDialog(false); try { // 선택된 사용자에 대한 임시 인증 세션 생성 요청 const response = await fetch('/api/auth/select-sgips-user', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId: user.userId, email: user.email, name: user.name }), }); const result = await response.json(); if (!response.ok || !result.success) { toast({ title: t('errorTitle'), description: result.error || t('mfaSetupError'), variant: 'destructive', }); return; } // 임시 인증 세션 생성 성공, MFA 진행 await proceedWithSelectedUser(user, result.tempAuthKey); } catch (error) { console.error('User selection error:', error); toast({ title: t('errorTitle'), description: t('mfaSetupError'), variant: 'destructive', }); } }; // MFA 화면에서 뒤로 가기 const handleBackToLogin = () => { setShowMfaForm(false); setMfaToken(''); setTempAuthKey(''); setMfaUserId(null); setMfaUserEmail(''); setMfaUserName(''); setMfaType('sms'); // 기본값으로 초기화 setMfaCountdown(0); setSelectedOtpUser(null); setShowUserSelectionDialog(false); setOtpUsers([]); }; // 세션 로딩 중이거나 이미 인증된 상태에서는 로딩 표시 if (status === 'loading') { return ( ); } // 이미 인증된 상태에서는 빈 화면 (리다이렉트 중) if (status === 'authenticated' && session?.user) { return ( ); } 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')}

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

{mfaType === 'email' ? '이메일 인증' : t('smsVerification')}

{selectedOtpUser ? t('firstAuthCompleteForSgips', { name: selectedOtpUser.name, email: mfaUserEmail }) : mfaType === 'email' ? `${mfaUserEmail}로 인증번호가 전송되었습니다.` : t('firstAuthCompleteFor', { email: mfaUserEmail }) }

{mfaType === 'email' ? '이메일에서 받은 6자리 인증번호를 입력해주세요.' : t('enterSixDigitCodeInstructions')}

)}
{/* 로그인 폼 또는 MFA 폼 */} {!showMfaForm ? ( <> {/* Login Method Tabs */}
{/* S-Gips 로그인은 영문 페이지에서 비활성화 0925 구매 요청사항*/} {lng !== 'en' && ( )}
{/* Username Login Form */} {loginMethod === 'username' && (
setUsername(e.target.value)} disabled={isFirstAuthLoading} />
setPassword(e.target.value)} disabled={isFirstAuthLoading} />
)} {/* S-Gips Login Form - 영문 페이지에서 비활성화 0925 구매 요청사항*/} {loginMethod === 'sgips' && lng !== 'en' && (
setSgipsUsername(e.target.value)} disabled={isFirstAuthLoading} />
setSgipsPassword(e.target.value)} disabled={isFirstAuthLoading} />

{t('sgipsAutoSms')}

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

{t('resendCode')}

{mfaType === 'email' ? '이메일을 받지 못하셨나요?' : t('didNotReceiveCode')}

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

{t('didNotReceiveCode')}

  • {t('checkPhoneNumber')}
  • {t('checkSpamFolder')}
  • {t('useResendButton')}
)} {/* OTP 사용자 선택 다이얼로그 */} {showUserSelectionDialog && !showMfaForm && (

{t('selectUser')}

{t('selectUserDescription')}

{otpUsers.map((user, index) => (
handleUserSelection(user)} >
{user.name}
{user.email}
{user.phone}
Vendor: {user.vndrcd}
))}
)} {/* 비밀번호 재설정 다이얼로그 */} {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")}”

) }