+ {" "}
{t(
"msg.admin.notice.scope",
diff --git a/adminfront/src/components/ui/badge.tsx b/adminfront/src/components/ui/badge.tsx
index 8ef586fd..8f9d0789 100644
--- a/adminfront/src/components/ui/badge.tsx
+++ b/adminfront/src/components/ui/badge.tsx
@@ -26,7 +26,8 @@ const badgeVariants = cva(
);
export interface BadgeProps
- extends React.HTMLAttributes,
+ extends
+ React.HTMLAttributes,
VariantProps {}
function Badge({ className, variant, ...props }: BadgeProps) {
diff --git a/adminfront/src/components/ui/button.tsx b/adminfront/src/components/ui/button.tsx
index ee1a84b4..91d21e58 100644
--- a/adminfront/src/components/ui/button.tsx
+++ b/adminfront/src/components/ui/button.tsx
@@ -34,7 +34,8 @@ const buttonVariants = cva(
);
export interface ButtonProps
- extends React.ButtonHTMLAttributes,
+ extends
+ React.ButtonHTMLAttributes,
VariantProps {
asChild?: boolean;
}
diff --git a/adminfront/src/components/ui/input.tsx b/adminfront/src/components/ui/input.tsx
index 41955477..eb2a9c6e 100644
--- a/adminfront/src/components/ui/input.tsx
+++ b/adminfront/src/components/ui/input.tsx
@@ -1,8 +1,7 @@
import * as React from "react";
import { cn } from "../../lib/utils";
-export interface InputProps
- extends React.InputHTMLAttributes {}
+export interface InputProps extends React.InputHTMLAttributes {}
const Input = React.forwardRef(
({ className, type, ...props }, ref) => {
diff --git a/adminfront/src/components/ui/textarea.tsx b/adminfront/src/components/ui/textarea.tsx
index 80f0abc2..78dbae91 100644
--- a/adminfront/src/components/ui/textarea.tsx
+++ b/adminfront/src/components/ui/textarea.tsx
@@ -1,8 +1,7 @@
import * as React from "react";
import { cn } from "../../lib/utils";
-export interface TextareaProps
- extends React.TextareaHTMLAttributes {}
+export interface TextareaProps extends React.TextareaHTMLAttributes {}
const Textarea = React.forwardRef(
({ className, ...props }, ref) => {
diff --git a/adminfront/src/features/auth/AuthCallbackPage.tsx b/adminfront/src/features/auth/AuthCallbackPage.tsx
new file mode 100644
index 00000000..bf8b5fda
--- /dev/null
+++ b/adminfront/src/features/auth/AuthCallbackPage.tsx
@@ -0,0 +1,43 @@
+import { ShieldHalf } from "lucide-react";
+import { useEffect } from "react";
+import { useNavigate, useSearchParams } from "react-router-dom";
+
+function AuthCallbackPage() {
+ const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
+
+ useEffect(() => {
+ const token = searchParams.get("token");
+ if (token) {
+ window.localStorage.setItem("admin_session", token);
+
+ // 만약 팝업창에서 실행 중이라면 부모 창에 알리고 닫기
+ if (window.opener) {
+ window.opener.postMessage({ type: "LOGIN_SUCCESS", token }, "*");
+ window.close();
+ } else {
+ // 일반 리다이렉트 방식인 경우 홈으로 이동
+ navigate("/", { replace: true });
+ }
+ } else {
+ console.error("No token found in callback URL");
+ navigate("/login", { replace: true });
+ }
+ }, [navigate, searchParams]);
+
+ return (
+
+
+
+
+
+
인증 완료 중...
+
+ 세션을 동기화하고 있습니다.
+
+
+
+ );
+}
+
+export default AuthCallbackPage;
diff --git a/adminfront/src/features/auth/LoginPage.tsx b/adminfront/src/features/auth/LoginPage.tsx
new file mode 100644
index 00000000..9939f443
--- /dev/null
+++ b/adminfront/src/features/auth/LoginPage.tsx
@@ -0,0 +1,132 @@
+import { ExternalLink, LogIn, ShieldHalf } from "lucide-react";
+import { useEffect, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { Button } from "../../components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "../../components/ui/card";
+
+function LoginPage() {
+ const navigate = useNavigate();
+ const [isLoggingIn, setIsLoggingIn] = useState(false);
+
+ useEffect(() => {
+ // Listen for login success message from the popup
+ const handleMessage = (event: MessageEvent) => {
+ // Security check: In production, verify event.origin
+ if (event.data?.type === "LOGIN_SUCCESS" && event.data?.token) {
+ window.localStorage.setItem("admin_session", event.data.token);
+ setIsLoggingIn(false);
+ navigate("/");
+ }
+ };
+
+ window.addEventListener("message", handleMessage);
+ return () => window.removeEventListener("message", handleMessage);
+ }, [navigate]);
+
+ const handleSSOLogin = () => {
+ const userfrontUrl = import.meta.env.USERFRONT_URL || "https://sso.hmac.kr";
+ const callbackUrl = `${window.location.origin}/auth/callback`;
+
+ // 항상 redirect_uri를 포함하여 로그인이 성공하면 콜백 페이지로 오도록 함
+ const loginUrl = `${userfrontUrl}/signin?source=adminfront&redirect_uri=${encodeURIComponent(callbackUrl)}`;
+
+ const width = 500;
+ const height = 700;
+ const left = window.screen.width / 2 - width / 2;
+ const top = window.screen.height / 2 - height / 2;
+
+ const popup = window.open(
+ loginUrl,
+ "BaronSSOLogin",
+ `width=${width},height=${height},top=${top},left=${left},status=no,menubar=no,toolbar=no`,
+ );
+
+ if (popup) {
+ setIsLoggingIn(true);
+ const timer = setInterval(() => {
+ if (popup.closed) {
+ clearInterval(timer);
+ setIsLoggingIn(false);
+ }
+ }, 1000);
+ } else {
+ alert("팝업 차단이 설정되어 있습니다. 팝업 허용 후 다시 시도해 주세요.");
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
Baron SSO
+
+ Admin Control Plane
+
+
+
+
+
+
+
+
+ 관리자 로그인
+
+
+ Baron 통합 인증(SSO)을 통해 관리자 페이지에 접속합니다.
+
+
+
+
+
+
+ 관리자 전역 세션은 보안을 위해 15분간 유지됩니다.
+
+ 민감한 작업 시 재인증을 요구할 수 있습니다.
+
+
+
+
+
+
+
+ 인증 정보가 없거나 로그인이 되지 않는 경우
+
+ 시스템 관리자에게 문의하세요.
+
+
+
+ );
+}
+
+export default LoginPage;
diff --git a/adminfront/src/features/overview/GlobalOverviewPage.tsx b/adminfront/src/features/overview/GlobalOverviewPage.tsx
index 61a0af2d..7db5a1cd 100644
--- a/adminfront/src/features/overview/GlobalOverviewPage.tsx
+++ b/adminfront/src/features/overview/GlobalOverviewPage.tsx
@@ -17,6 +17,7 @@ import {
CardTitle,
} from "../../components/ui/card";
import { t } from "../../lib/i18n";
+import PermissionChecker from "./components/PermissionChecker";
const summaryCards = [
{
@@ -216,6 +217,8 @@ function GlobalOverviewPage() {