'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, getSession } 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({ className, ...props }: React.ComponentProps<"div">) { 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 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: '재설정 링크 전송', 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) { throw new Error(result.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: 'SMS 전송 완료', description: '인증번호를 전송했습니다.', }); } else { const errorData = await response.json(); toast({ title: t('errorTitle'), description: errorData.message || 'SMS 전송에 실패했습니다.', variant: 'destructive', }); } } catch (error) { console.error('SMS send error:', error); toast({ title: t('errorTitle'), description: 'SMS 전송 중 오류가 발생했습니다.', variant: 'destructive', }); } finally { setIsSmsLoading(false); } }; // MFA 토큰 검증 및 최종 로그인 const handleMfaSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!mfaToken || mfaToken.length !== 6) { toast({ title: t('errorTitle'), description: '6자리 인증번호를 입력해주세요.', variant: 'destructive', }); return; } if (!tempAuthKey) { toast({ title: t('errorTitle'), description: '인증 세션이 만료되었습니다. 다시 로그인해주세요.', 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: '인증 완료', description: '로그인이 완료되었습니다.', }); // 콜백 URL 처리 const callbackUrlParam = searchParams?.get('callbackUrl'); if (callbackUrlParam) { try { const callbackUrl = new URL(callbackUrlParam); const relativeUrl = callbackUrl.pathname + callbackUrl.search; router.push(relativeUrl); } catch (e) { router.push(callbackUrlParam); } } else { router.push(`/${lng}/partners/dashboard`); } } else { let errorMessage = '인증번호가 올바르지 않습니다.'; if (result?.error) { switch (result.error) { case 'CredentialsSignin': errorMessage = '인증번호가 올바르지 않거나 만료되었습니다.'; break; case 'AccessDenied': errorMessage = '접근이 거부되었습니다.'; break; default: errorMessage = 'MFA 인증에 실패했습니다.'; } } toast({ title: t('errorTitle'), description: errorMessage, variant: 'destructive', }); } } catch (error) { console.error('MFA verification error:', error); toast({ title: t('errorTitle'), description: 'MFA 인증 중 오류가 발생했습니다.', 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: '1차 인증 완료', description: 'SMS 인증을 진행합니다.', }); // MFA 화면으로 전환 setTempAuthKey(authResult.tempAuthKey); setMfaUserId(authResult.userId); setMfaUserEmail(authResult.email); setShowMfaForm(true); // 자동으로 SMS 전송 (userId 직접 전달) setTimeout(() => { handleSendSms(authResult.userId); }, 500); toast({ title: 'SMS 인증 필요', description: '등록된 전화번호로 인증번호를 전송합니다.', }); } } catch (error: any) { console.error('Username login error:', error); let errorMessage = t('invalidCredentials'); if (error.message) { errorMessage = error.message; } 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: 'S-Gips 인증 완료', description: 'SMS 인증을 진행합니다.', }); // MFA 화면으로 전환 setTempAuthKey(authResult.tempAuthKey); setMfaUserId(authResult.userId); setMfaUserEmail(authResult.email); setShowMfaForm(true); // 자동으로 SMS 전송 (userId 직접 전달) setTimeout(() => { handleSendSms(authResult.userId); }, 500); toast({ title: 'SMS 인증 시작', description: 'S-Gips 등록 전화번호로 인증번호를 전송합니다.', }); } } catch (error: any) { console.error('S-Gips login error:', error); let errorMessage = t('sgipsLoginFailed'); if (error.message) { errorMessage = error.message; } 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
{'업체 등록 신청'}
{/* Content section that occupies remaining space, centered vertically */}
{/* Header */}
{!showMfaForm ? ( <>

{t('loginMessage')}

{'등록된 업체만 로그인하실 수 있습니다. 아직 등록되지 않은 업체라면 상단의 업체 등록 신청 버튼을 이용해주세요.'}

) : ( <>
🔐

SMS 인증

{mfaUserEmail}로 1차 인증이 완료되었습니다

등록된 전화번호로 전송된 6자리 인증번호를 입력해주세요

)}
{/* 로그인 폼 또는 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} />

S-Gips 계정으로 로그인하면 자동으로 SMS 인증이 진행됩니다.

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

인증번호 재전송

인증번호를 받지 못하셨나요?

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

인증번호를 받지 못하셨나요?

  • 전화번호가 올바른지 확인해주세요
  • 스팸 메시지함을 확인해주세요
  • 잠시 후 재전송 버튼을 이용해주세요
)} {/* 비밀번호 재설정 다이얼로그 */} {showForgotPassword && !showMfaForm && (

비밀번호 재설정

가입하신 이메일 주소를 입력하시면 비밀번호 재설정 링크를 보내드립니다.

)} {/* 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")}”

) }