diff options
| -rw-r--r-- | app/[lng]/evcp/(evcp)/system/layout.tsx | 2 | ||||
| -rw-r--r-- | components/common/loading/animation.css | 336 | ||||
| -rw-r--r-- | components/common/loading/animation.tsx | 14 | ||||
| -rw-r--r-- | components/common/loading/loading.tsx | 17 | ||||
| -rw-r--r-- | components/login/login-form-shi.tsx | 32 | ||||
| -rw-r--r-- | components/login/login-form.tsx | 42 |
6 files changed, 440 insertions, 3 deletions
diff --git a/app/[lng]/evcp/(evcp)/system/layout.tsx b/app/[lng]/evcp/(evcp)/system/layout.tsx index 2776ed8b..2933b298 100644 --- a/app/[lng]/evcp/(evcp)/system/layout.tsx +++ b/app/[lng]/evcp/(evcp)/system/layout.tsx @@ -68,7 +68,7 @@ export default async function SettingsLayout({ <aside className="-mx-4 lg:w-1/5"> <SidebarNav items={sidebarNavItems} /> </aside> - <div className="flex-1 ">{children}</div> + <div className="flex-1 min-w-0 overflow-hidden">{children}</div> </div> </div> </section> diff --git a/components/common/loading/animation.css b/components/common/loading/animation.css new file mode 100644 index 00000000..a560449c --- /dev/null +++ b/components/common/loading/animation.css @@ -0,0 +1,336 @@ +.loader { + position: relative; + width: 75px; + height: 100px; +} + +.loader__bar { + position: absolute; + bottom: 0; + width: 10px; + height: 50%; + background: rgb(0, 0, 0); + transform-origin: center bottom; + box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.2); +} + +.loader__bar:nth-child(1) { + left: 0px; + transform: scale(1, 0.2); + -webkit-animation: barUp1 4s infinite; + animation: barUp1 4s infinite; +} + +.loader__bar:nth-child(2) { + left: 15px; + transform: scale(1, 0.4); + -webkit-animation: barUp2 4s infinite; + animation: barUp2 4s infinite; +} + +.loader__bar:nth-child(3) { + left: 30px; + transform: scale(1, 0.6); + -webkit-animation: barUp3 4s infinite; + animation: barUp3 4s infinite; +} + +.loader__bar:nth-child(4) { + left: 45px; + transform: scale(1, 0.8); + -webkit-animation: barUp4 4s infinite; + animation: barUp4 4s infinite; +} + +.loader__bar:nth-child(5) { + left: 60px; + transform: scale(1, 1); + -webkit-animation: barUp5 4s infinite; + animation: barUp5 4s infinite; +} + +.loader__ball { + position: absolute; + bottom: 10px; + left: 0; + width: 10px; + height: 10px; + background: rgb(44, 12, 255); + border-radius: 50%; + -webkit-animation: ball624 4s infinite; + animation: ball624 4s infinite; +} + +@keyframes ball624 { + 0% { + transform: translate(0, 0); + } + + 5% { + transform: translate(8px, -14px); + } + + 10% { + transform: translate(15px, -10px); + } + + 17% { + transform: translate(23px, -24px); + } + + 20% { + transform: translate(30px, -20px); + } + + 27% { + transform: translate(38px, -34px); + } + + 30% { + transform: translate(45px, -30px); + } + + 37% { + transform: translate(53px, -44px); + } + + 40% { + transform: translate(60px, -40px); + } + + 50% { + transform: translate(60px, 0); + } + + 57% { + transform: translate(53px, -14px); + } + + 60% { + transform: translate(45px, -10px); + } + + 67% { + transform: translate(37px, -24px); + } + + 70% { + transform: translate(30px, -20px); + } + + 77% { + transform: translate(22px, -34px); + } + + 80% { + transform: translate(15px, -30px); + } + + 87% { + transform: translate(7px, -44px); + } + + 90% { + transform: translate(0, -40px); + } + + 100% { + transform: translate(0, 0); + } +} + +@-webkit-keyframes barUp1 { + 0% { + transform: scale(1, 0.2); + } + + 40% { + transform: scale(1, 0.2); + } + + 50% { + transform: scale(1, 1); + } + + 90% { + transform: scale(1, 1); + } + + 100% { + transform: scale(1, 0.2); + } +} + +@keyframes barUp1 { + 0% { + transform: scale(1, 0.2); + } + + 40% { + transform: scale(1, 0.2); + } + + 50% { + transform: scale(1, 1); + } + + 90% { + transform: scale(1, 1); + } + + 100% { + transform: scale(1, 0.2); + } +} + +@-webkit-keyframes barUp2 { + 0% { + transform: scale(1, 0.4); + } + + 40% { + transform: scale(1, 0.4); + } + + 50% { + transform: scale(1, 0.8); + } + + 90% { + transform: scale(1, 0.8); + } + + 100% { + transform: scale(1, 0.4); + } +} + +@keyframes barUp2 { + 0% { + transform: scale(1, 0.4); + } + + 40% { + transform: scale(1, 0.4); + } + + 50% { + transform: scale(1, 0.8); + } + + 90% { + transform: scale(1, 0.8); + } + + 100% { + transform: scale(1, 0.4); + } +} + +@-webkit-keyframes barUp3 { + 0% { + transform: scale(1, 0.6); + } + + 100% { + transform: scale(1, 0.6); + } +} + +@keyframes barUp3 { + 0% { + transform: scale(1, 0.6); + } + + 100% { + transform: scale(1, 0.6); + } +} + +@-webkit-keyframes barUp4 { + 0% { + transform: scale(1, 0.8); + } + + 40% { + transform: scale(1, 0.8); + } + + 50% { + transform: scale(1, 0.4); + } + + 90% { + transform: scale(1, 0.4); + } + + 100% { + transform: scale(1, 0.8); + } +} + +@keyframes barUp4 { + 0% { + transform: scale(1, 0.8); + } + + 40% { + transform: scale(1, 0.8); + } + + 50% { + transform: scale(1, 0.4); + } + + 90% { + transform: scale(1, 0.4); + } + + 100% { + transform: scale(1, 0.8); + } +} + +@-webkit-keyframes barUp5 { + 0% { + transform: scale(1, 1); + } + + 40% { + transform: scale(1, 1); + } + + 50% { + transform: scale(1, 0.2); + } + + 90% { + transform: scale(1, 0.2); + } + + 100% { + transform: scale(1, 1); + } +} + +@keyframes barUp5 { + 0% { + transform: scale(1, 1); + } + + 40% { + transform: scale(1, 1); + } + + 50% { + transform: scale(1, 0.2); + } + + 90% { + transform: scale(1, 0.2); + } + + 100% { + transform: scale(1, 1); + } +}
\ No newline at end of file diff --git a/components/common/loading/animation.tsx b/components/common/loading/animation.tsx new file mode 100644 index 00000000..042ecafc --- /dev/null +++ b/components/common/loading/animation.tsx @@ -0,0 +1,14 @@ +import './animation.css'; + +export default function Animation() { + return ( + <div className="loader"> + <div className="loader__bar"></div> + <div className="loader__bar"></div> + <div className="loader__bar"></div> + <div className="loader__bar"></div> + <div className="loader__bar"></div> + <div className="loader__ball"></div> + </div> + ); +}
\ No newline at end of file diff --git a/components/common/loading/loading.tsx b/components/common/loading/loading.tsx new file mode 100644 index 00000000..d5345ba3 --- /dev/null +++ b/components/common/loading/loading.tsx @@ -0,0 +1,17 @@ +import Animation from "./animation"; + +export default function Loading({message}: {message: string}) { + return ( + <> + <div className="container relative flex h-screen flex-col items-center justify-center"> + <div className="flex items-center space-x-2"> + <span className="text-md font-bold">eVCP</span> + </div> + <div> + <Animation /> + </div> + <div className="mt-4 text-sm text-muted-foreground">{message}</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 862f9f8a..1a554cbc 100644 --- a/components/login/login-form-shi.tsx +++ b/components/login/login-form-shi.tsx @@ -15,13 +15,14 @@ import { InputOTPGroup, InputOTPSlot, } from "@/components/ui/input-otp" -import { signIn } from 'next-auth/react'; +import { signIn, useSession } 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 import { KnoxSSOButton } from './saml-login-button'; // SAML 로그인 버튼 import +import Loading from "../common/loading/loading"; export function LoginFormSHI({ className, @@ -39,6 +40,7 @@ export function LoginFormSHI({ const { t, i18n } = useTranslation(lng, 'login'); const { toast } = useToast(); + const { data: session, status } = useSession(); const handleChangeLanguage = (lang: string) => { const segments = pathname.split('/'); @@ -181,6 +183,34 @@ export function LoginFormSHI({ verifyToken(); }, [token, toast, t]); + // 이미 로그인된 사용자 리다이렉트 처리 + useEffect(() => { + if (status === 'authenticated' && session?.user) { + const callbackUrlParam = searchParams?.get('callbackUrl'); + + if (callbackUrlParam) { + try { + const callbackUrl = new URL(callbackUrlParam); + const relativeUrl = callbackUrl.pathname + callbackUrl.search; + router.push(relativeUrl); + } catch { + router.push(callbackUrlParam); + } + } else { + router.push(`/${lng}/evcp/report`); + } + } + }, [status, session, router, lng, searchParams]); + + // 세션 로딩 중이거나 이미 인증된 상태에서는 로딩 표시 + if (status === 'loading') { + return <Loading message={t('loading')} />; + } + + if (status === 'authenticated' && session?.user) { + return <Loading message={t('redirecting')} />; + } + 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 */} diff --git a/components/login/login-form.tsx b/components/login/login-form.tsx index 7453edb6..6aca755f 100644 --- a/components/login/login-form.tsx +++ b/components/login/login-form.tsx @@ -9,7 +9,7 @@ import { useToast } from "@/hooks/use-toast"; import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem } from "@/components/ui/dropdown-menu" import { useTranslation } from '@/i18n/client' import { useRouter, useParams, usePathname, useSearchParams } from 'next/navigation'; -import { signIn } from 'next-auth/react'; +import { signIn, useSession } from 'next-auth/react'; import { buttonVariants } from "@/components/ui/button" import Link from "next/link" import Image from 'next/image'; @@ -20,6 +20,7 @@ import { InputOTPSlot, } from "@/components/ui/input-otp" import { requestPasswordResetAction } from "@/lib/users/auth/partners-auth"; +import Loading from "../common/loading/loading"; type LoginMethod = 'username' | 'sgips'; @@ -32,6 +33,7 @@ export function LoginForm() { const lng = params.lng as string; const { t, i18n } = useTranslation(lng, 'login'); const { toast } = useToast(); + const { data: session, status } = useSession(); // 상태 관리 const [loginMethod, setLoginMethod] = useState<LoginMethod>('username'); @@ -64,6 +66,30 @@ export function LoginForm() { message: undefined, }); + // 이미 로그인된 사용자 리다이렉트 처리 + useEffect(() => { + if (status === 'authenticated' && session?.user) { + 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 { + // 유효하지 않은 URL이면 그대로 사용 (이미 상대 경로일 수 있음) + router.push(callbackUrlParam); + } + } else { + // callbackUrl이 없으면 기본 대시보드로 리다이렉트 + router.push(`/${lng}/partners/report`); + } + } + }, [status, session, router, lng, searchParams]); + const handleChangeLanguage = (lang: string) => { const segments = pathname.split('/'); segments[1] = lang; @@ -404,6 +430,20 @@ export function LoginForm() { setMfaCountdown(0); }; + // 세션 로딩 중이거나 이미 인증된 상태에서는 로딩 표시 + if (status === 'loading') { + return ( + <Loading message={t('loading')} /> + ); + } + + // 이미 인증된 상태에서는 빈 화면 (리다이렉트 중) + if (status === 'authenticated' && session?.user) { + return ( + <Loading message={t('redirecting')} /> + ); + } + 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 */} |
