diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-03-26 00:37:41 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-03-26 00:37:41 +0000 |
| commit | e0dfb55c5457aec489fc084c4567e791b4c65eb1 (patch) | |
| tree | 68543a65d88f5afb3a0202925804103daa91bc6f /components/login/login-form.tsx | |
3/25 까지의 대표님 작업사항
Diffstat (limited to 'components/login/login-form.tsx')
| -rw-r--r-- | components/login/login-form.tsx | 323 |
1 files changed, 323 insertions, 0 deletions
diff --git a/components/login/login-form.tsx b/components/login/login-form.tsx new file mode 100644 index 00000000..41d232f8 --- /dev/null +++ b/components/login/login-form.tsx @@ -0,0 +1,323 @@ +'use client'; + +import { useState, useEffect } from "react"; +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { Card, CardContent } from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { SendIcon, Loader2, GlobeIcon, ChevronDownIcon, Ship } 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 { + InputOTP, + InputOTPGroup, + InputOTPSlot, +} from "@/components/ui/input-otp" +import { signIn } from 'next-auth/react'; +import { sendOtpAction } from "@/lib/users/send-otp"; +import { verifyTokenAction } from "@/lib/users/verifyToken"; +import { buttonVariants } from "@/components/ui/button" +import Link from "next/link" +import Image from 'next/image'; // 추가: Image 컴포넌트 import + +export function LoginForm({ + className, + ...props +}: React.ComponentProps<"div">) { + + const params = useParams() || {}; + const pathname = usePathname() || ''; + const router = useRouter(); + const searchParams = useSearchParams(); + const token = searchParams?.get('token') || null; + + + const lng = params.lng as string; + const { t, i18n } = useTranslation(lng, 'login'); + + const { toast } = useToast(); + + 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 [email, setEmail] = useState(''); + const [otpSent, setOtpSent] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [otp, setOtp] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + try { + const result = await sendOtpAction(email, lng); + + if (result?.success) { + setOtpSent(true); + toast({ + title: t('otpSentTitle'), + description: t('otpSentMessage'), + }); + } + } catch (error) { + toast({ + title: t('errorTitle'), + description: t('defaultErrorMessage'), + variant: 'destructive', + }); + } finally { + setIsLoading(false); + } + }; + + + async function handleOtpSubmit(e: React.FormEvent) { + e.preventDefault(); + setIsLoading(true); + + try { + // next-auth의 Credentials Provider로 로그인 시도 + const result = await signIn('credentials', { + email, + code: otp, + redirect: false, // 커스텀 처리 위해 redirect: false + }); + + if (result?.ok) { + // 토스트 메시지 표시 + toast({ + title: t('loginSuccess'), + description: t('youAreLoggedIn'), + }); + + // NextAuth에서 유저 정보 API 호출 (최신 상태 보장) + const response = await fetch('/api/auth/session'); + const session = await response.json(); + + // domain 값에 따라 동적으로 리다이렉션 + const userDomain = session?.user?.domain; + console.log(session) + + if (userDomain === 'evcp') { + router.push(`/${lng}/evcp/report`); + } else if (userDomain === 'partners') { + router.push(`/${lng}/partners/dashboard`); + } else { + // 기본 리다이렉션 경로 + router.push(`/${lng}/dashboard`); + } + } else { + toast({ + title: t('errorTitle'), + description: t('defaultErrorMessage'), + variant: 'destructive', + }); + } + } catch (error) { + console.error('Login error:', error); + toast({ + title: t('errorTitle'), + description: t('defaultErrorMessage'), + variant: 'destructive', + }); + } finally { + setIsLoading(false); + } + } + + useEffect(() => { + const verifyToken = async () => { + if (!token) return; + setIsLoading(true); + + try { + const data = await verifyTokenAction(token); + + if (data.valid) { + setOtpSent(true); + setEmail(data.email ?? ''); + } else { + toast({ + title: t('errorTitle'), + description: t('invalidToken'), + variant: 'destructive', + }); + } + } catch (error) { + toast({ + title: t('errorTitle'), + description: t('defaultErrorMessage'), + variant: 'destructive', + }); + } finally { + setIsLoading(false); + } + }; + verifyToken(); + }, [token, toast, t]); + + return ( + <div className="container relative flex h-screen flex-col items-center justify-center md:grid lg:max-w-none lg:grid-cols-2 lg:px-0"> + {/* Left Content */} + <div className="flex flex-col w-full h-screen lg:p-2"> + {/* Top bar with Logo + eVCP (left) and "Request Vendor Repository" (right) */} + <div className="flex items-center justify-between"> + <div className="flex items-center space-x-2"> + {/* <img + src="/images/logo.png" + alt="logo" + className="h-8 w-auto" + /> */} + <Ship className="w-4 h-4" /> + <span className="text-md font-bold">eVCP</span> + </div> + <Link + href="/partners/repository" + className={cn(buttonVariants({ variant: "ghost" }))} + > + Request Vendor Repository + </Link> + </div> + + {/* Content section that occupies remaining space, centered vertically */} + <div className="flex-1 flex items-center justify-center"> + {/* Your form container */} + <div className="mx-auto w-full flex flex-col space-y-6 sm:w-[350px]"> + + {/* Here's your existing login/OTP forms: */} + {!otpSent ? ( + <form onSubmit={handleSubmit} className="p-6 md:p-8"> + <div className="flex flex-col gap-6"> + <div className="flex flex-col items-center text-center"> + <h1 className="text-2xl font-bold">{t('loginMessage')}</h1> + </div> + <div className="grid gap-2"> + <Input + id="email" + type="email" + placeholder={t('email')} + required + className="h-10" + value={email} + onChange={(e) => setEmail(e.target.value)} + /> + </div> + <Button type="submit" className="w-full" variant="samsung" disabled={isLoading}> + {isLoading ? t('sending') : t('ContinueWithEmail')} + </Button> + <div className="text-center text-sm mx-auto"> + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="ghost" className="flex items-center gap-2"> + <GlobeIcon className="h-4 w-4" /> + <span>{currentLanguageText}</span> + <ChevronDownIcon className="h-4 w-4" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuRadioGroup + value={i18n.language} + onValueChange={(value) => handleChangeLanguage(value)} + > + <DropdownMenuRadioItem value="en"> + {t('languages.english')} + </DropdownMenuRadioItem> + <DropdownMenuRadioItem value="ko"> + {t('languages.korean')} + </DropdownMenuRadioItem> + </DropdownMenuRadioGroup> + </DropdownMenuContent> + </DropdownMenu> + </div> + </div> + </form> + ) : ( + <form onSubmit={handleOtpSubmit} className="flex flex-col gap-4 p-6 md:p-8"> + <div className="flex flex-col gap-6"> + <div className="flex flex-col items-center text-center"> + <h1 className="text-2xl font-bold">{t('loginMessage')}</h1> + </div> + <div className="grid gap-2 justify-center"> + <InputOTP + maxLength={6} + value={otp} + onChange={(value) => setOtp(value)} + > + <InputOTPGroup> + <InputOTPSlot index={0} /> + <InputOTPSlot index={1} /> + <InputOTPSlot index={2} /> + <InputOTPSlot index={3} /> + <InputOTPSlot index={4} /> + <InputOTPSlot index={5} /> + </InputOTPGroup> + </InputOTP> + </div> + <Button type="submit" className="w-full" disabled={isLoading}> + {isLoading ? t('verifying') : t('verifyOtp')} + </Button> + <div className="mx-auto"> + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="ghost" className="flex items-center gap-2"> + <GlobeIcon className="h-4 w-4" /> + <span>{currentLanguageText}</span> + <ChevronDownIcon className="h-4 w-4" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuRadioGroup + value={i18n.language} + onValueChange={(value) => handleChangeLanguage(value)} + > + <DropdownMenuRadioItem value="en"> + {t('languages.english')} + </DropdownMenuRadioItem> + <DropdownMenuRadioItem value="ko"> + {t('languages.korean')} + </DropdownMenuRadioItem> + </DropdownMenuRadioGroup> + </DropdownMenuContent> + </DropdownMenu> + </div> + </div> + </form> + )} + + <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 hover:[&_a]:text-primary"> + {t('termsMessage')} <a href="#">{t('termsOfService')}</a> {t('and')} + <a href="#">{t('privacyPolicy')}</a>. + </div> + </div> + </div> + </div> + + {/* Right BG 이미지 영역 - Image 컴포넌트로 수정 */} + <div className="relative hidden h-full flex-col p-10 text-white dark:border-r md:flex"> + {/* Image 컴포넌트로 대체 */} + <div className="absolute inset-0"> + <Image + src="/images/02.jpg" + alt="Background image" + fill + priority + sizes="(max-width: 1024px) 100vw, 50vw" + className="object-cover" + /> + </div> + <div className="relative z-10 mt-auto"> + <blockquote className="space-y-2"> + <p className="text-sm">“{t("blockquote")}”</p> + {/* <footer className="text-sm">SHI</footer> */} + </blockquote> + </div> + </div> + </div> + ) +}
\ No newline at end of file |
