diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-04-28 02:13:30 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-04-28 02:13:30 +0000 |
| commit | ef4c533ebacc2cdc97e518f30e9a9350004fcdfb (patch) | |
| tree | 345251a3ed0f4429716fa5edaa31024d8f4cb560 /components/login | |
| parent | 9ceed79cf32c896f8a998399bf1b296506b2cd4a (diff) | |
~20250428 작업사항
Diffstat (limited to 'components/login')
| -rw-r--r-- | components/login/login-form copy 2.tsx | 470 | ||||
| -rw-r--r-- | components/login/login-form copy.tsx | 468 | ||||
| -rw-r--r-- | components/login/login-form-shi.tsx | 34 | ||||
| -rw-r--r-- | components/login/login-form.tsx | 67 | ||||
| -rw-r--r-- | components/login/partner-auth-form.tsx | 128 |
5 files changed, 1125 insertions, 42 deletions
diff --git a/components/login/login-form copy 2.tsx b/components/login/login-form copy 2.tsx new file mode 100644 index 00000000..d5ac01b9 --- /dev/null +++ b/components/login/login-form copy 2.tsx @@ -0,0 +1,470 @@ +'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, InfoIcon, ArrowRight } 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'; +import { + Alert, + AlertDescription, + AlertTitle, +} from "@/components/ui/alert"; + +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 [showCredentialsForm, setShowCredentialsForm] = useState(false); + // 새로운 상태: 업체 등록 안내 표시 여부 + const [showVendorRegistrationInfo, setShowVendorRegistrationInfo] = useState(false); + + 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 [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + + // 업체 등록 페이지로 이동하는 함수 + const goToVendorRegistration = () => { + router.push(`/${lng}/partners/repository`); + }; + + 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'), + }); + } else { + // Handle specific error types + let errorMessage = t('defaultErrorMessage'); + + // 업체 미등록 사용자 에러 처리 + if (result.error === 'userNotFound' || result.error === 'vendorNotRegistered') { + setShowVendorRegistrationInfo(true); + errorMessage = t('vendorNotRegistered') || '등록된 업체가 아닙니다. 먼저 업체 등록 신청을 해주세요.'; + } + + toast({ + title: t('errorTitle'), + description: result.message || errorMessage, + variant: 'destructive', + }); + } + } catch (error) { + // This will catch network errors or other unexpected issues + console.error(error); + toast({ + title: t('errorTitle'), + description: t('networkErrorMessage'), + 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'), + }); + + router.push(`/${lng}/partners/dashboard`); + } else { + // 로그인 실패 시 에러 메시지에 업체 등록 관련 정보 포함 + if (result?.error === 'vendorNotRegistered') { + setShowVendorRegistrationInfo(true); + toast({ + title: t('errorTitle'), + description: t('vendorNotRegistered') || '등록된 업체가 아닙니다. 먼저 업체 등록 신청을 해주세요.', + variant: 'destructive', + }); + } 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); + } + } + + // 새로운 로그인 처리 함수 추가 + const handleCredentialsLogin = async () => { + if (!username || !password) { + toast({ + title: t('errorTitle'), + description: t('credentialsRequired'), + variant: 'destructive', + }); + return; + } + + setIsLoading(true); + + try { + // next-auth의 다른 credentials provider로 로그인 시도 + const result = await signIn('credentials-password', { + username, + password, + redirect: false, + }); + + if (result?.ok) { + toast({ + title: t('loginSuccess'), + description: t('youAreLoggedIn'), + }); + + router.push(`/${lng}/partners/dashboard`); + } else { + // 로그인 실패 시 업체 등록 관련 정보 표시 여부 결정 + if (result?.error === 'vendorNotRegistered') { + setShowVendorRegistrationInfo(true); + toast({ + title: t('errorTitle'), + description: t('vendorNotRegistered') || '등록된 업체가 아닙니다. 먼저 업체 등록 신청을 해주세요.', + variant: 'destructive', + }); + } else { + toast({ + title: t('errorTitle'), + description: t('invalidCredentials'), + 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 p-4"> + <div className="flex items-center space-x-2"> + <Ship className="w-4 h-4" /> + <span className="text-md font-bold">eVCP</span> + </div> + + {/* 업체 등록 신청 버튼 - 가시성 향상을 위해 variant 변경 */} + <Link + href={`/${lng}/partners/repository`} + className={cn(buttonVariants({ variant: "outline" }), "border-blue-500 text-blue-600 hover:bg-blue-50")} + > + <InfoIcon className="w-4 h-4 mr-2" /> + {t('registerVendor') || '업체 등록 신청'} + </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]"> + {/* 업체 등록 안내 알림 - 특정 상황에서만 표시 */} + {showVendorRegistrationInfo && ( + <Alert className="border-blue-500 bg-blue-50"> + <InfoIcon className="h-4 w-4 text-blue-600" /> + <AlertTitle className="text-blue-700"> + {t('vendorRegistrationRequired') || '업체 등록이 필요합니다'} + </AlertTitle> + <AlertDescription className="text-blue-600"> + {t('vendorRegistrationMessage') || '로그인하시려면 먼저 업체 등록이 필요합니다. 아래 버튼을 클릭하여 등록을 진행해주세요.'} + </AlertDescription> + <Button + onClick={goToVendorRegistration} + className="mt-2 w-full bg-blue-600 hover:bg-blue-700 text-white" + > + {t('goToVendorRegistration') || '업체 등록 신청하기'} + <ArrowRight className="ml-2 h-4 w-4" /> + </Button> + </Alert> + )} + + <form onSubmit={handleOtpSubmit} 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> + + {/* 설명 텍스트 추가 - 업체 등록 관련 안내 */} + <p className="text-sm text-muted-foreground mt-2"> + {t('loginDescription') || '등록된 업체만 로그인하실 수 있습니다. 아직 등록되지 않은 업체라면 상단의 업체 등록 신청 버튼을 이용해주세요.'} + </p> + </div> + + {/* S-chips 로그인 폼이 표시되지 않을 때만 이메일 입력 필드 표시 */} + {!showCredentialsForm && ( + <> + <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} + onClick={handleSubmit} + > + {isLoading ? t('sending') : t('ContinueWithEmail')} + </Button> + + {/* 구분선과 "Or continue with" 섹션 추가 */} + <div className="relative"> + <div className="absolute inset-0 flex items-center"> + <span className="w-full border-t"></span> + </div> + <div className="relative flex justify-center text-xs uppercase"> + <span className="bg-background px-2 text-muted-foreground"> + {t('orContinueWith')} + </span> + </div> + </div> + + {/* S-chips 로그인 버튼 */} + <Button + type="button" + className="w-full" + onClick={() => setShowCredentialsForm(true)} + > + S-Gips로 로그인하기 + </Button> + + {/* 업체 등록 안내 링크 추가 */} + <Button + type="button" + variant="link" + className="text-blue-600 hover:text-blue-800" + onClick={goToVendorRegistration} + > + {t('newVendor') || '신규 업체이신가요? 여기서 등록하세요'} + </Button> + </> + )} + + {/* ID/비밀번호 로그인 폼 - 버튼 클릭 시에만 표시 */} + {showCredentialsForm && ( + <> + <div className="grid gap-4"> + <Input + id="username" + type="text" + placeholder="S-chips ID" + className="h-10" + value={username} + onChange={(e) => setUsername(e.target.value)} + /> + <Input + id="password" + type="password" + placeholder="비밀번호" + className="h-10" + value={password} + onChange={(e) => setPassword(e.target.value)} + /> + <Button + type="button" + className="w-full" + variant="samsung" + onClick={handleCredentialsLogin} + disabled={isLoading} + > + {isLoading ? "로그인 중..." : "로그인"} + </Button> + + {/* 뒤로 가기 버튼 */} + <Button + type="button" + variant="ghost" + className="w-full text-sm" + onClick={() => setShowCredentialsForm(false)} + > + 이메일로 로그인하기 + </Button> + + {/* 업체 등록 안내 링크 추가 */} + <Button + type="button" + variant="link" + className="text-blue-600 hover:text-blue-800" + onClick={goToVendorRegistration} + > + {t('newVendor') || '신규 업체이신가요? 여기서 등록하세요'} + </Button> + </div> + </> + )} + + <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> + + <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 diff --git a/components/login/login-form copy.tsx b/components/login/login-form copy.tsx new file mode 100644 index 00000000..ef9eba10 --- /dev/null +++ b/components/login/login-form copy.tsx @@ -0,0 +1,468 @@ +'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, InfoIcon } 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 [showCredentialsForm, setShowCredentialsForm] = useState(false); + + + 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 [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + + const goToVendorRegistration = () => { + router.push(`/${lng}/partners/repository`); + }; + + 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'), + }); + } else { + // Handle specific error types + let errorMessage = t('defaultErrorMessage'); + + // You can handle different error types differently + if (result.error === 'userNotFound') { + errorMessage = t('userNotFoundMessage'); + } + + toast({ + title: t('errorTitle'), + description: result.message || errorMessage, + variant: 'destructive', + }); + } + } catch (error) { + // This will catch network errors or other unexpected issues + console.error(error); + toast({ + title: t('errorTitle'), + description: t('networkErrorMessage'), + 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'), + }); + + router.push(`/${lng}/partners/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); + } + } + + // 새로운 로그인 처리 함수 추가 + const handleCredentialsLogin = async () => { + if (!username || !password) { + toast({ + title: t('errorTitle'), + description: t('credentialsRequired'), + variant: 'destructive', + }); + return; + } + + setIsLoading(true); + + try { + // next-auth의 다른 credentials provider로 로그인 시도 + const result = await signIn('credentials-password', { + username, + password, + redirect: false, + }); + + if (result?.ok) { + toast({ + title: t('loginSuccess'), + description: t('youAreLoggedIn'), + }); + + router.push(`/${lng}/partners/dashboard`); + } else { + toast({ + title: t('errorTitle'), + description: t('invalidCredentials'), + 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" }))} + > + <InfoIcon className="w-4 h-4 mr-1" /> + {'업체 등록 신청'} + </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"> */} + <form onSubmit={handleOtpSubmit} 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> + + {/* 설명 텍스트 추가 - 업체 등록 관련 안내 */} + <p className="text-xs text-muted-foreground mt-2"> + {'등록된 업체만 로그인하실 수 있습니다. 아직 등록되지 않은 업체라면 상단의 업체 등록 신청 버튼을 이용해주세요.'} + </p> + </div> + + {/* S-Gips 로그인 폼이 표시되지 않을 때만 이메일 입력 필드 표시 */} + {!showCredentialsForm && ( + <> + <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> + + {/* 구분선과 "Or continue with" 섹션 추가 */} + <div className="relative"> + <div className="absolute inset-0 flex items-center"> + <span className="w-full border-t"></span> + </div> + <div className="relative flex justify-center text-xs uppercase"> + <span className="bg-background px-2 text-muted-foreground"> + {t('orContinueWith')} + </span> + </div> + </div> + + {/* S-Gips 로그인 버튼 */} + <Button + type="button" + className="w-full" + // variant="" + onClick={() => setShowCredentialsForm(true)} + > + S-Gips로 로그인하기 + </Button> + + {/* 업체 등록 안내 링크 추가 */} + <Button + type="button" + variant="link" + className="text-blue-600 hover:text-blue-800" + onClick={goToVendorRegistration} + > + {'신규 업체이신가요? 여기서 등록하세요'} + </Button> + </> + )} + + {/* ID/비밀번호 로그인 폼 - 버튼 클릭 시에만 표시 */} + {showCredentialsForm && ( + <> + <div className="grid gap-4"> + <Input + id="username" + type="text" + placeholder="S-Gips ID" + className="h-10" + value={username} + onChange={(e) => setUsername(e.target.value)} + /> + <Input + id="password" + type="password" + placeholder="비밀번호" + className="h-10" + value={password} + onChange={(e) => setPassword(e.target.value)} + /> + <Button + type="button" + className="w-full" + variant="samsung" + onClick={handleCredentialsLogin} + disabled={isLoading} + > + {isLoading ? "로그인 중..." : "로그인"} + </Button> + + {/* 뒤로 가기 버튼 */} + <Button + type="button" + variant="ghost" + className="w-full text-sm" + onClick={() => setShowCredentialsForm(false)} + > + 이메일로 로그인하기 + </Button> + </div> + </> + )} + + <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 diff --git a/components/login/login-form-shi.tsx b/components/login/login-form-shi.tsx index fb985592..ef39d122 100644 --- a/components/login/login-form-shi.tsx +++ b/components/login/login-form-shi.tsx @@ -110,8 +110,25 @@ export function LoginFormSHI({ title: t('loginSuccess'), description: t('youAreLoggedIn'), }); - - router.push(`/${lng}/evcp/report`); + + 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 (e) { + // 유효하지 않은 URL이면 그대로 사용 (이미 상대 경로일 수 있음) + router.push(callbackUrlParam); + } + } else { + // callbackUrl이 없으면 기본 대시보드로 리다이렉트 + router.push(`/${lng}/evcp/report`); + } } else { toast({ @@ -186,8 +203,10 @@ export function LoginFormSHI({ <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"> + {/* {!otpSent ? + ( */} + {/* <form onSubmit={handleSubmit} className="p-6 md:p-8"> */} + <form onSubmit={handleOtpSubmit} 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> @@ -232,7 +251,10 @@ export function LoginFormSHI({ </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"> @@ -283,7 +305,7 @@ export function LoginFormSHI({ </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')} diff --git a/components/login/login-form.tsx b/components/login/login-form.tsx index 92fa6e2c..7236c02e 100644 --- a/components/login/login-form.tsx +++ b/components/login/login-form.tsx @@ -5,7 +5,7 @@ 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 { SendIcon, Loader2, GlobeIcon, ChevronDownIcon, Ship, InfoIcon } 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' @@ -55,6 +55,10 @@ export function LoginForm({ const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); + const goToVendorRegistration = () => { + router.push(`/${lng}/partners/repository`); + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); @@ -114,7 +118,24 @@ export function LoginForm({ description: t('youAreLoggedIn'), }); - router.push(`/${lng}/partners/dashboard`); + 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 (e) { + // 유효하지 않은 URL이면 그대로 사용 (이미 상대 경로일 수 있음) + router.push(callbackUrlParam); + } + } else { + // callbackUrl이 없으면 기본 대시보드로 리다이렉트 + router.push(`/${lng}/partners/dashboard`); + } } else { toast({ @@ -232,7 +253,8 @@ export function LoginForm({ href="/partners/repository" className={cn(buttonVariants({ variant: "ghost" }))} > - Request Vendor Repository + <InfoIcon className="w-4 h-4 mr-1" /> + {'업체 등록 신청'} </Link> </div> @@ -242,14 +264,21 @@ export function LoginForm({ <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"> + {/* {!otpSent ? ( */} + + {/* <form onSubmit={handleSubmit} className="p-6 md:p-8"> */} + <form onSubmit={handleOtpSubmit} 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="flex flex-col items-center text-center"> + <h1 className="text-2xl font-bold">{t('loginMessage')}</h1> + + {/* 설명 텍스트 추가 - 업체 등록 관련 안내 */} + <p className="text-xs text-muted-foreground mt-2"> + {'등록된 업체만 로그인하실 수 있습니다. 아직 등록되지 않은 업체라면 상단의 업체 등록 신청 버튼을 이용해주세요.'} + </p> + </div> - {/* S-chips 로그인 폼이 표시되지 않을 때만 이메일 입력 필드 표시 */} + {/* S-Gips 로그인 폼이 표시되지 않을 때만 이메일 입력 필드 표시 */} {!showCredentialsForm && ( <> <div className="grid gap-2"> @@ -279,15 +308,25 @@ export function LoginForm({ </div> </div> - {/* S-chips 로그인 버튼 */} + {/* S-Gips 로그인 버튼 */} <Button type="button" className="w-full" // variant="" onClick={() => setShowCredentialsForm(true)} > - S-chips로 로그인하기 + S-Gips로 로그인하기 </Button> + + {/* 업체 등록 안내 링크 추가 */} + <Button + type="button" + variant="link" + className="text-blue-600 hover:text-blue-800" + onClick={goToVendorRegistration} + > + {'신규 업체이신가요? 여기서 등록하세요'} + </Button> </> )} @@ -298,7 +337,7 @@ export function LoginForm({ <Input id="username" type="text" - placeholder="S-chips ID" + placeholder="S-Gips ID" className="h-10" value={username} onChange={(e) => setUsername(e.target.value)} @@ -360,7 +399,7 @@ export function LoginForm({ </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"> @@ -411,7 +450,7 @@ export function LoginForm({ </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')} diff --git a/components/login/partner-auth-form.tsx b/components/login/partner-auth-form.tsx index effd7bd3..ada64d96 100644 --- a/components/login/partner-auth-form.tsx +++ b/components/login/partner-auth-form.tsx @@ -16,11 +16,22 @@ import { DropdownMenuRadioItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" -import { GlobeIcon, ChevronDownIcon, Loader, Ship } from "lucide-react" +import { GlobeIcon, ChevronDownIcon, Loader, Ship, LogIn, InfoIcon, HelpCircle } 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 { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { + Alert, + AlertDescription, + AlertTitle, +} from "@/components/ui/alert" import { checkJoinPortal } from "@/lib/vendors/service" import Image from "next/image" @@ -30,11 +41,13 @@ interface UserAuthFormProps extends React.HTMLAttributes<HTMLDivElement> { } export function CompanyAuthForm({ className, ...props }: UserAuthFormProps) { const [isLoading, setIsLoading] = React.useState<boolean>(false) + const [showInfoBanner, setShowInfoBanner] = React.useState<boolean>(true) const router = useRouter() const { toast } = useToast() - const params = useParams() - const pathname = usePathname() + const params = useParams() || {}; + const pathname = usePathname() || ''; + const lng = params.lng as string const { t, i18n } = useTranslation(lng, "login") @@ -51,6 +64,11 @@ export function CompanyAuthForm({ className, ...props }: UserAuthFormProps) { ? t("languages.japanese") : t("languages.english") + // 로그인 페이지로 이동 + const goToLogin = () => { + router.push(`/${lng}/partners`); + } + // --------------------------- // 1) onSubmit -> 서버 액션 호출 // --------------------------- @@ -86,11 +104,26 @@ export function CompanyAuthForm({ className, ...props }: UserAuthFormProps) { // 가입 가능 → signup 페이지 이동 router.push(`/partners/signup?taxID=${taxID}`) } else { + // 이미 등록된 기업인 경우 - 로그인으로 안내하는 토스트와 함께 추가 액션 제공 toast({ variant: "destructive", title: "가입이 진행 중이거나 완료된 회사", description: `${result.data} 에 연락하여 계정 생성 요청을 하시기 바랍니다.`, }) + + // 로그인 액션 버튼이 있는 알림 표시 + setTimeout(() => { + toast({ + title: "이미 등록된 회사이신가요?", + description: "로그인 페이지로 이동하여 계정에 접속하세요.", + action: ( + <Button variant="outline" onClick={goToLogin} className="bg-blue-50 border-blue-300"> + <LogIn className="mr-2 h-4 w-4" /> + 로그인하기 + </Button> + ), + }) + }, 1000); } } catch (error: any) { console.error(error) @@ -111,43 +144,80 @@ export function CompanyAuthForm({ className, ...props }: UserAuthFormProps) { <div className="flex flex-col w-full h-screen lg:p-2"> {/* Top bar */} - <div className="flex items-center justify-between"> + <div className="flex items-center justify-between p-4"> <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> - {/* Remove 'absolute right-4 top-4 ...', just use buttonVariants */} + {/* 로그인 버튼 가시성 개선 */} <Link - href="/login" + href={`/${lng}/partners`} className={cn( - buttonVariants({ variant: "ghost" }) + buttonVariants({ variant: "outline" }), + "border-blue-500 text-blue-600 hover:bg-blue-50" )} > - Login + <LogIn className="mr-2 h-4 w-4" /> + {t("login") || "로그인"} </Link> </div> <div className="flex-1 flex items-center justify-center"> - <div className="mx-auto w-full flex flex-col space-y-6 sm:w-[350px]"> + <div className="mx-auto w-full flex flex-col space-y-6 sm:w-[400px]"> + {/* 정보 알림 배너 - 업체 등록과 로그인의 관계 설명 */} + {showInfoBanner && ( + <Alert className="bg-blue-50 border-blue-200"> + <InfoIcon className="h-4 w-4 text-blue-600" /> + <AlertTitle className="text-blue-700 mt-1"> + {t("registrationInfoTitle") || "업체 등록 신청 안내"} + </AlertTitle> + <AlertDescription className="text-blue-600"> + {t("registrationInfoDescription") || "이미 등록된 업체의 직원이신가요? 상단의 로그인 버튼을 눌러 로그인하세요. 새로운 업체 등록을 원하시면 아래 양식을 작성해주세요."} + </AlertDescription> + <Button + variant="ghost" + size="sm" + onClick={() => setShowInfoBanner(false)} + className="absolute top-2 right-4 h-6 w-6 p-0" + > + ✕ + </Button> + </Alert> + )} + <div className="flex flex-col space-y-2 text-center"> <h1 className="text-2xl font-semibold tracking-tight"> - {t("heading")} + {t("heading") || "업체 등록 신청"} </h1> - <p className="text-sm text-muted-foreground">{t("subheading")}</p> + <p className="text-sm text-muted-foreground"> + {t("subheading") || "귀사의 사업자 등록 번호를 입력하여 등록을 시작하세요"} + </p> </div> <div className={cn("grid gap-6", className)} {...props}> <form onSubmit={onSubmit}> - <div className="grid gap-2"> - <div className="grid gap-1"> - <label className="sr-only" htmlFor="taxid"> - Business Number / Tax ID - </label> + <div className="grid gap-4"> + <div className="grid gap-2"> + <div className="flex items-center justify-between"> + <Label htmlFor="taxid"> + 사업자등록번호 / Tax ID + </Label> + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <Button variant="ghost" size="icon" className="h-6 w-6 p-0"> + <HelpCircle className="h-4 w-4 text-muted-foreground" /> + <span className="sr-only">Help</span> + </Button> + </TooltipTrigger> + <TooltipContent> + <p className="max-w-xs"> + {t("taxIdTooltip") || "법인/개인사업자 사업자등록번호를 '-' 포함하여 입력해주세요 (예: 123-45-67890)"} + </p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + </div> <input id="taxid" name="taxid" @@ -159,12 +229,26 @@ export function CompanyAuthForm({ className, ...props }: UserAuthFormProps) { disabled={isLoading} className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50" /> + <p className="text-xs text-muted-foreground"> + {t("taxIdHint") || "사업자 등록 번호는 업체 인증에 사용됩니다"} + </p> </div> <Button type="submit" disabled={isLoading} variant="samsung"> {isLoading && <Loader className="mr-2 h-4 w-4 animate-spin" />} - {t("joinButton")} + {t("joinButton") || "업체 등록 시작하기"} </Button> + {/* 로그인 안내 링크 추가 */} + <div className="text-center"> + <Button + variant="link" + className="text-blue-600 hover:text-blue-800 text-sm" + onClick={goToLogin} + > + {t("alreadyRegistered") || "이미 등록된 업체이신가요? 로그인하기"} + </Button> + </div> + {/* 언어 선택 Dropdown */} <div className="mx-auto"> <DropdownMenu> |
