From e0dfb55c5457aec489fc084c4567e791b4c65eb1 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Wed, 26 Mar 2025 00:37:41 +0000 Subject: 3/25 까지의 대표님 작업사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/login/login-form-skeleton.tsx | 71 +++++++ components/login/login-form.tsx | 323 +++++++++++++++++++++++++++++++ components/login/partner-auth-form.tsx | 241 +++++++++++++++++++++++ 3 files changed, 635 insertions(+) create mode 100644 components/login/login-form-skeleton.tsx create mode 100644 components/login/login-form.tsx create mode 100644 components/login/partner-auth-form.tsx (limited to 'components/login') diff --git a/components/login/login-form-skeleton.tsx b/components/login/login-form-skeleton.tsx new file mode 100644 index 00000000..c434c4b7 --- /dev/null +++ b/components/login/login-form-skeleton.tsx @@ -0,0 +1,71 @@ +import { Skeleton } from "@/components/ui/skeleton" +import { Button } from "@/components/ui/button" +import { Ship, Loader2, GlobeIcon, ChevronDownIcon } from "lucide-react" +import Link from "next/link" +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +export function LoginFormSkeleton() { + return ( +
+ {/* Left Content */} +
+ {/* Top bar with Logo + eVCP (left) and "Request Vendor Repository" (right) */} +
+
+ + eVCP +
+ + Request Vendor Repository + +
+ + {/* Content section that occupies remaining space, centered vertically */} +
+ {/* Form container */} +
+
+
+
+ +
+
+ +
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + {/* Right BG 이미지 영역 */} +
+
+
+ {/* Optional top-right content on the image side */} +
+
+
+ +
+
+
+
+ ) +} \ No newline at end of file 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 ( +
+ {/* Left Content */} +
+ {/* Top bar with Logo + eVCP (left) and "Request Vendor Repository" (right) */} +
+
+ {/* logo */} + + eVCP +
+ + Request Vendor Repository + +
+ + {/* Content section that occupies remaining space, centered vertically */} +
+ {/* Your form container */} +
+ + {/* Here's your existing login/OTP forms: */} + {!otpSent ? ( +
+
+
+

{t('loginMessage')}

+
+
+ setEmail(e.target.value)} + /> +
+ +
+ + + + + + handleChangeLanguage(value)} + > + + {t('languages.english')} + + + {t('languages.korean')} + + + + +
+
+
+ ) : ( +
+
+
+

{t('loginMessage')}

+
+
+ setOtp(value)} + > + + + + + + + + + +
+ +
+ + + + + + handleChangeLanguage(value)} + > + + {t('languages.english')} + + + {t('languages.korean')} + + + + +
+
+
+ )} + +
+ {t('termsMessage')} {t('termsOfService')} {t('and')} + {t('privacyPolicy')}. +
+
+
+
+ + {/* Right BG 이미지 영역 - Image 컴포넌트로 수정 */} +
+ {/* Image 컴포넌트로 대체 */} +
+ Background image +
+
+
+

“{t("blockquote")}”

+ {/*
SHI
*/} +
+
+
+
+ ) +} \ No newline at end of file diff --git a/components/login/partner-auth-form.tsx b/components/login/partner-auth-form.tsx new file mode 100644 index 00000000..effd7bd3 --- /dev/null +++ b/components/login/partner-auth-form.tsx @@ -0,0 +1,241 @@ +"use client" + +import * as React from "react" +import { useToast } from "@/hooks/use-toast" +import { useRouter, useParams, usePathname } from "next/navigation" +import { useTranslation } from "@/i18n/client" +import Link from "next/link" + +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { GlobeIcon, ChevronDownIcon, Loader, Ship } from "lucide-react" +import { languages } from "@/config/language" +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" +import { siteConfig } from "@/config/site" + +import { checkJoinPortal } from "@/lib/vendors/service" +import Image from "next/image" +// ↑ 실제 경로 맞춤 수정 (ex: "@/app/[lng]/actions/joinPortal" 등) + +interface UserAuthFormProps extends React.HTMLAttributes { } + +export function CompanyAuthForm({ className, ...props }: UserAuthFormProps) { + const [isLoading, setIsLoading] = React.useState(false) + const router = useRouter() + const { toast } = useToast() + const params = useParams() + const pathname = usePathname() + + const lng = params.lng as string + const { t, i18n } = useTranslation(lng, "login") + + const handleChangeLanguage = (lang: string) => { + const segments = pathname.split("/") + segments[1] = lang + router.push(segments.join("/")) + } + + const currentLanguageText = + i18n.language === "ko" + ? t("languages.korean") + : i18n.language === "ja" + ? t("languages.japanese") + : t("languages.english") + + // --------------------------- + // 1) onSubmit -> 서버 액션 호출 + // --------------------------- + async function onSubmit(event: React.FormEvent) { + event.preventDefault() + setIsLoading(true) + + const formData = new FormData(event.currentTarget) + const taxID = formData.get("taxid")?.toString().trim() + + if (!taxID) { + toast({ + variant: "destructive", + title: "오류", + description: "Tax ID를 입력해주세요.", + }) + setIsLoading(false) + return + } + + try { + // --------------------------- + // 2) 서버 액션 호출 + // --------------------------- + const result = await checkJoinPortal(taxID) + + if (result.success) { + toast({ + variant: "default", + title: "성공", + description: "가입 신청이 가능합니다", + }) + // 가입 가능 → signup 페이지 이동 + router.push(`/partners/signup?taxID=${taxID}`) + } else { + toast({ + variant: "destructive", + title: "가입이 진행 중이거나 완료된 회사", + description: `${result.data} 에 연락하여 계정 생성 요청을 하시기 바랍니다.`, + }) + } + } catch (error: any) { + console.error(error) + toast({ + variant: "destructive", + title: "오류", + description: "서버 액션 호출에 실패했습니다. 잠시 후 다시 시도해주세요.", + }) + } finally { + setIsLoading(false) + } + } + + return ( +
+ + {/* Left BG 이미지 영역 */} + +
+ {/* Top bar */} +
+
+ {/* logo */} + + eVCP +
+ + {/* Remove 'absolute right-4 top-4 ...', just use buttonVariants */} + + Login + +
+
+
+
+

+ {t("heading")} +

+

{t("subheading")}

+
+ +
+
+
+
+ + +
+ + + {/* 언어 선택 Dropdown */} +
+ + + + + + handleChangeLanguage(value)} + > + {languages.map((v) => ( + + {t(v.labelKey)} + + ))} + + + +
+
+
+
+

+ {t("agreement")}{" "} + + {t("termsOfService")} + {" "} + {t("and")}{" "} + + {t("privacyPolicy")} + + . +

+
+
+ +
+ + + {/* Right Content */} +
+ {/* Image 컴포넌트로 대체 */} +
+ Background image +
+
+
+

“{t("blockquote")}”

+ {/* */} +
+
+
+ +
+ ) +} \ No newline at end of file -- cgit v1.2.3