summaryrefslogtreecommitdiff
path: root/components/login
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-04-28 02:13:30 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-04-28 02:13:30 +0000
commitef4c533ebacc2cdc97e518f30e9a9350004fcdfb (patch)
tree345251a3ed0f4429716fa5edaa31024d8f4cb560 /components/login
parent9ceed79cf32c896f8a998399bf1b296506b2cd4a (diff)
~20250428 작업사항
Diffstat (limited to 'components/login')
-rw-r--r--components/login/login-form copy 2.tsx470
-rw-r--r--components/login/login-form copy.tsx468
-rw-r--r--components/login/login-form-shi.tsx34
-rw-r--r--components/login/login-form.tsx67
-rw-r--r--components/login/partner-auth-form.tsx128
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">&ldquo;{t("blockquote")}&rdquo;</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">&ldquo;{t("blockquote")}&rdquo;</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>