summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/[lng]/evcp/(evcp)/system/layout.tsx2
-rw-r--r--components/common/loading/animation.css336
-rw-r--r--components/common/loading/animation.tsx14
-rw-r--r--components/common/loading/loading.tsx17
-rw-r--r--components/login/login-form-shi.tsx32
-rw-r--r--components/login/login-form.tsx42
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 */}