1
0
forked from baron/baron-sso

admin/dev 사이드바 프레임 공통화

This commit is contained in:
2026-05-15 10:16:37 +09:00
parent 0327409631
commit 0bf8089120
5 changed files with 263 additions and 223 deletions

View File

@@ -21,12 +21,14 @@ import { useEffect, useRef, useState } from "react";
import { useAuth } from "react-oidc-context";
import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom";
import {
AppSidebar,
type ShellTranslator,
applyShellTheme,
buildShellProfileSummary,
buildShellSessionStatus,
readShellSessionExpiryEnabled,
readShellTheme,
type ShellSidebarNavItem,
shellLayoutClasses,
writeShellSessionExpiryEnabled,
} from "../../../../common/shell";
@@ -41,19 +43,38 @@ import {
import LanguageSelector from "../common/LanguageSelector";
import RoleSwitcher from "./RoleSwitcher";
interface NavItem {
label: string;
to: string;
icon: React.ComponentType<{ size?: number | string }>;
isExternal?: boolean;
}
const staticNavItems: NavItem[] = [
{ label: "ui.admin.nav.overview", to: "/", icon: LayoutDashboard },
{ label: "ui.admin.nav.users", to: "/users", icon: Users },
{ label: "ui.admin.nav.api_keys", to: "/api-keys", icon: Key },
{ label: "ui.admin.nav.audit_logs", to: "/audit-logs", icon: NotebookTabs },
{ label: "ui.admin.nav.auth_guard", to: "/auth", icon: KeyRound },
const staticNavItems: ShellSidebarNavItem[] = [
{
labelKey: "ui.admin.nav.overview",
labelFallback: "Overview",
to: "/",
icon: LayoutDashboard,
end: true,
},
{
labelKey: "ui.admin.nav.users",
labelFallback: "Users",
to: "/users",
icon: Users,
},
{
labelKey: "ui.admin.nav.api_keys",
labelFallback: "API Keys",
to: "/api-keys",
icon: Key,
},
{
labelKey: "ui.admin.nav.audit_logs",
labelFallback: "Audit Logs",
to: "/audit-logs",
icon: NotebookTabs,
},
{
labelKey: "ui.admin.nav.auth_guard",
labelFallback: "Auth Guard",
to: "/auth",
icon: KeyRound,
},
];
type SessionStatusProps = {
@@ -145,7 +166,7 @@ function AppLayout() {
._IS_TEST_MODE === true,
});
const navItems = React.useMemo(() => {
const navItems = React.useMemo<ShellSidebarNavItem[]>(() => {
const items = [...staticNavItems];
const isTest =
(window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean })
@@ -167,36 +188,42 @@ function AppLayout() {
if (isSuperAdmin) {
filteredItems.splice(1, 0, {
label: "ui.admin.nav.tenants",
labelKey: "ui.admin.nav.tenants",
labelFallback: "Tenants",
to: "/tenants",
icon: Building2,
});
filteredItems.splice(2, 0, {
label: "ui.admin.nav.org_chart",
labelKey: "ui.admin.nav.org_chart",
labelFallback: "Org Chart",
to: orgfrontUrl,
icon: Network,
isExternal: true,
});
filteredItems.splice(4, 0, {
label: "ui.admin.nav.user_projection",
labelKey: "ui.admin.nav.user_projection",
labelFallback: "User Projection",
to: "/system/projections/users",
icon: Database,
});
filteredItems.splice(5, 0, {
label: "ui.admin.nav.data_integrity",
labelKey: "ui.admin.nav.data_integrity",
labelFallback: "Data Integrity",
to: "/system/data-integrity",
icon: ShieldCheck,
});
} else if (isTenantAdmin || manageableCount > 0) {
if (manageableCount <= 1 && profile?.tenantId) {
filteredItems.splice(1, 0, {
label: "ui.admin.nav.my_tenant",
labelKey: "ui.admin.nav.my_tenant",
labelFallback: "My Tenant",
to: `/tenants/${profile.tenantId}`,
icon: Building2,
});
} else if (manageableCount > 1) {
filteredItems.splice(1, 0, {
label: "ui.admin.nav.tenants",
labelKey: "ui.admin.nav.tenants",
labelFallback: "Tenants",
to: "/tenants",
icon: Building2,
});
@@ -205,7 +232,8 @@ function AppLayout() {
manageableCount <= 1 && profile?.tenantId ? 2 : 2,
0,
{
label: "ui.admin.nav.org_chart",
labelKey: "ui.admin.nav.org_chart",
labelFallback: "Org Chart",
to: orgfrontUrl,
icon: Network,
isExternal: true,
@@ -214,7 +242,8 @@ function AppLayout() {
} else {
// 일반 사용자(Tenant Member)도 조직도 메뉴를 볼 수 있도록 추가합니다.
filteredItems.splice(1, 0, {
label: "ui.admin.nav.org_chart",
labelKey: "ui.admin.nav.org_chart",
labelFallback: "Org Chart",
to: orgfrontUrl,
icon: Network,
isExternal: true,
@@ -442,6 +471,66 @@ function AppLayout() {
return next;
});
};
const sidebarNavContent = (
<div className={shellLayoutClasses.navList}>
{navItems.map((item) => {
const { labelKey, labelFallback, to, icon: Icon, isExternal } = item;
if (isExternal) {
return (
<a
key={to}
href={to}
target="_blank"
rel="noopener noreferrer"
className={[
shellLayoutClasses.navItemBase,
shellLayoutClasses.navItemIdle,
].join(" ")}
>
<Icon size={18} />
<span>{t(labelKey, labelFallback)}</span>
</a>
);
}
return (
<NavLink
key={to}
to={to}
end={item.end ?? to === "/"}
className={({ isActive }) =>
[
shellLayoutClasses.navItemBase,
item.isActive !== undefined
? item.isActive
? shellLayoutClasses.navItemActive
: shellLayoutClasses.navItemIdle
: isActive
? shellLayoutClasses.navItemActive
: shellLayoutClasses.navItemIdle,
].join(" ")
}
>
<Icon size={18} />
<span>{t(labelKey, labelFallback)}</span>
</NavLink>
);
})}
</div>
);
const sidebarFooterContent = (
<div className="border-t border-border/50 px-3 pt-4">
<button
type="button"
onClick={handleLogout}
className={shellLayoutClasses.logoutButton}
>
<LogOut size={18} />
<span>{t("ui.admin.nav.logout", "Logout")}</span>
</button>
</div>
);
if (auth.isLoading) {
return (
@@ -453,84 +542,13 @@ function AppLayout() {
return (
<div className={shellLayoutClasses.root}>
<aside className={shellLayoutClasses.asideStatic}>
<div className={shellLayoutClasses.brandSection}>
<div className={shellLayoutClasses.brandWrap}>
<div className={shellLayoutClasses.brandIcon}>
<ShieldHalf size={20} />
</div>
<div>
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">
{t("ui.admin.brand", "Baron 로그인")}
</p>
<h1 className="text-lg font-semibold">
{t("ui.admin.title", "Admin Control")}
</h1>
</div>
</div>
</div>
<nav className={shellLayoutClasses.navWrap}>
<div className={shellLayoutClasses.navList}>
{navItems.map((item: NavItem) => {
const { label, to, icon: Icon, isExternal } = item;
const isOrgChart = location.pathname === "/tenants/org-chart";
const isTenantsRoot = to === "/tenants";
const isCustomActive = isTenantsRoot
? location.pathname.startsWith("/tenants") && !isOrgChart
: to === "/"
? location.pathname === "/"
: location.pathname.startsWith(to);
if (isExternal) {
return (
<a
key={to}
href={to}
target="_blank"
rel="noopener noreferrer"
className={[
shellLayoutClasses.navItemBase,
shellLayoutClasses.navItemIdle,
].join(" ")}
>
<Icon size={18} />
<span>{t(label, label)}</span>
</a>
);
}
return (
<NavLink
key={to}
to={to}
className={() =>
[
shellLayoutClasses.navItemBase,
isCustomActive
? shellLayoutClasses.navItemActive
: shellLayoutClasses.navItemIdle,
].join(" ")
}
>
<Icon size={18} />
<span>{t(label, label)}</span>
</NavLink>
);
})}
</div>
<div className="border-t border-border/50 px-3 pt-4">
<button
type="button"
onClick={handleLogout}
className={shellLayoutClasses.logoutButton}
>
<LogOut size={18} />
<span>{t("ui.admin.nav.logout", "Logout")}</span>
</button>
</div>
</nav>
</aside>
<AppSidebar
brandLabel={t("ui.admin.brand", "Baron 로그인")}
brandTitle={t("ui.admin.title", "Admin Control")}
brandIcon={<ShieldHalf size={20} />}
navContent={sidebarNavContent}
footerContent={sidebarFooterContent}
/>
<div className={shellLayoutClasses.contentWide}>
<header className={shellLayoutClasses.headerElevated}>