1
0
forked from baron/baron-sso

사이드바 접기 기능 추가

This commit is contained in:
2026-06-04 15:56:33 +09:00
parent f6c7cb3b22
commit 1596342d03
16 changed files with 277 additions and 33 deletions

View File

@@ -127,6 +127,22 @@ describe("admin AppLayout", () => {
expect(worksmobileIcon.querySelector('path[fill="white"]')).toBeNull(); expect(worksmobileIcon.querySelector('path[fill="white"]')).toBeNull();
}); });
it("toggles the sidebar and persists the collapsed state", async () => {
renderLayout();
const collapseButton = await screen.findByRole("button", {
name: "사이드바 접기",
});
fireEvent.click(collapseButton);
expect(window.localStorage.getItem("baron_shell_sidebar_collapsed")).toBe(
"true",
);
expect(
screen.getByRole("button", { name: "사이드바 펼치기" }),
).toBeInTheDocument();
});
it("opens profile menu, navigates, toggles theme/session, and logs out", async () => { it("opens profile menu, navigates, toggles theme/session, and logs out", async () => {
renderLayout(); renderLayout();

View File

@@ -26,11 +26,13 @@ import {
buildShellProfileSummary, buildShellProfileSummary,
buildShellSessionStatus, buildShellSessionStatus,
readShellSessionExpiryEnabled, readShellSessionExpiryEnabled,
readShellSidebarCollapsed,
readShellTheme, readShellTheme,
type ShellSidebarNavItem, type ShellSidebarNavItem,
type ShellTranslator, type ShellTranslator,
shellLayoutClasses, shellLayoutClasses,
writeShellSessionExpiryEnabled, writeShellSessionExpiryEnabled,
writeShellSidebarCollapsed,
} from "../../../../common/shell"; } from "../../../../common/shell";
import { canAccessWorksmobile } from "../../features/tenants/routes/worksmobileAccess"; import { canAccessWorksmobile } from "../../features/tenants/routes/worksmobileAccess";
import { buildAuthenticatedOrgChartUrl } from "../../features/users/orgChartPicker"; import { buildAuthenticatedOrgChartUrl } from "../../features/users/orgChartPicker";
@@ -165,6 +167,9 @@ function AppLayout() {
const isDevelopmentRuntime = import.meta.env.MODE === "development"; const isDevelopmentRuntime = import.meta.env.MODE === "development";
const [theme, setTheme] = useState<"light" | "dark">(readShellTheme); const [theme, setTheme] = useState<"light" | "dark">(readShellTheme);
const [isProfileOpen, setIsProfileOpen] = useState(false); const [isProfileOpen, setIsProfileOpen] = useState(false);
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(() =>
readShellSidebarCollapsed(false),
);
const [isSessionExpiryEnabled, setIsSessionExpiryEnabled] = useState(() => const [isSessionExpiryEnabled, setIsSessionExpiryEnabled] = useState(() =>
readShellSessionExpiryEnabled(!isDevelopmentRuntime), readShellSessionExpiryEnabled(!isDevelopmentRuntime),
); );
@@ -508,10 +513,18 @@ function AppLayout() {
return next; return next;
}); });
}; };
const handleSidebarToggle = () => {
setIsSidebarCollapsed((prev) => {
const next = !prev;
writeShellSidebarCollapsed(next);
return next;
});
};
const sidebarNavContent = ( const sidebarNavContent = (
<div className={shellLayoutClasses.navList}> <div className={shellLayoutClasses.navList}>
{navItems.map((item) => { {navItems.map((item) => {
const { labelKey, labelFallback, to, icon: Icon, isExternal } = item; const { labelKey, labelFallback, to, icon: Icon, isExternal } = item;
const label = t(labelKey, labelFallback);
if (isExternal) { if (isExternal) {
return ( return (
@@ -522,11 +535,18 @@ function AppLayout() {
rel="noopener noreferrer" rel="noopener noreferrer"
className={[ className={[
shellLayoutClasses.navItemBase, shellLayoutClasses.navItemBase,
isSidebarCollapsed
? shellLayoutClasses.navItemBaseCollapsed
: "",
shellLayoutClasses.navItemIdle, shellLayoutClasses.navItemIdle,
].join(" ")} ].join(" ")}
title={label}
aria-label={label}
> >
<Icon size={18} /> <Icon size={18} />
<span>{t(labelKey, labelFallback)}</span> <span className={isSidebarCollapsed ? "sr-only" : ""}>
{label}
</span>
</a> </a>
); );
} }
@@ -539,6 +559,9 @@ function AppLayout() {
className={({ isActive }) => className={({ isActive }) =>
[ [
shellLayoutClasses.navItemBase, shellLayoutClasses.navItemBase,
isSidebarCollapsed
? shellLayoutClasses.navItemBaseCollapsed
: "",
item.isActive !== undefined item.isActive !== undefined
? item.isActive ? item.isActive
? shellLayoutClasses.navItemActive ? shellLayoutClasses.navItemActive
@@ -548,9 +571,11 @@ function AppLayout() {
: shellLayoutClasses.navItemIdle, : shellLayoutClasses.navItemIdle,
].join(" ") ].join(" ")
} }
title={label}
aria-label={label}
> >
<Icon size={18} /> <Icon size={18} />
<span>{t(labelKey, labelFallback)}</span> <span className={isSidebarCollapsed ? "sr-only" : ""}>{label}</span>
</NavLink> </NavLink>
); );
})} })}
@@ -561,10 +586,17 @@ function AppLayout() {
<button <button
type="button" type="button"
onClick={handleLogout} onClick={handleLogout}
className={shellLayoutClasses.logoutButton} className={
isSidebarCollapsed
? shellLayoutClasses.logoutButtonCollapsed
: shellLayoutClasses.logoutButton
}
title={t("ui.shell.nav.logout", "Logout")}
> >
<LogOut size={18} /> <LogOut size={18} />
<span>{t("ui.shell.nav.logout", "Logout")}</span> <span className={isSidebarCollapsed ? "sr-only" : ""}>
{t("ui.shell.nav.logout", "Logout")}
</span>
</button> </button>
</div> </div>
); );
@@ -578,13 +610,23 @@ function AppLayout() {
} }
return ( return (
<div className={shellLayoutClasses.root}> <div
className={
isSidebarCollapsed
? shellLayoutClasses.rootCollapsed
: shellLayoutClasses.root
}
>
<AppSidebar <AppSidebar
brandLabel={t("ui.admin.brand", "Baron 로그인")} brandLabel={t("ui.admin.brand", "Baron 로그인")}
brandTitle={t("ui.admin.title", "Admin Control")} brandTitle={t("ui.admin.title", "Admin Control")}
brandIcon={<ShieldHalf size={20} />} brandIcon={<ShieldHalf size={20} />}
navContent={sidebarNavContent} navContent={sidebarNavContent}
footerContent={sidebarFooterContent} footerContent={sidebarFooterContent}
collapsed={isSidebarCollapsed}
onToggleCollapsed={handleSidebarToggle}
collapseLabel={t("ui.shell.sidebar.collapse", "사이드바 접기")}
expandLabel={t("ui.shell.sidebar.expand", "사이드바 펼치기")}
/> />
<div className={shellLayoutClasses.contentWide}> <div className={shellLayoutClasses.contentWide}>

View File

@@ -1541,6 +1541,10 @@ unknown_name = "Unknown User"
logout = "Logout" logout = "Logout"
profile = "My Profile" profile = "My Profile"
[ui.shell.sidebar]
collapse = "Collapse sidebar"
expand = "Expand sidebar"
[ui.shell.role] [ui.shell.role]
rp_admin = "Service Administrator (RP Admin)" rp_admin = "Service Administrator (RP Admin)"
super_admin = "System Administrator (Super Admin)" super_admin = "System Administrator (Super Admin)"

View File

@@ -1544,6 +1544,10 @@ unknown_name = "Unknown User"
logout = "Logout" logout = "Logout"
profile = "내 정보" profile = "내 정보"
[ui.shell.sidebar]
collapse = "사이드바 접기"
expand = "사이드바 펼치기"
[ui.shell.role] [ui.shell.role]
rp_admin = "서비스 관리자 (RP Admin)" rp_admin = "서비스 관리자 (RP Admin)"
super_admin = "시스템 관리자 (Super Admin)" super_admin = "시스템 관리자 (Super Admin)"

View File

@@ -1513,6 +1513,10 @@ unknown_name = ""
logout = "" logout = ""
profile = "" profile = ""
[ui.shell.sidebar]
collapse = ""
expand = ""
[ui.shell.role] [ui.shell.role]
rp_admin = "" rp_admin = ""
super_admin = "" super_admin = ""

View File

@@ -1,3 +1,4 @@
import { Menu, SquareMenu } from "lucide-react";
import type { ComponentType, ReactNode } from "react"; import type { ComponentType, ReactNode } from "react";
import { shellLayoutClasses } from "./layout"; import { shellLayoutClasses } from "./layout";
@@ -14,9 +15,13 @@ export type ShellSidebarNavItem = {
type ShellSidebarProps = { type ShellSidebarProps = {
brandLabel: string; brandLabel: string;
brandTitle: string; brandTitle: string;
brandIcon: ReactNode; brandIcon?: ReactNode;
navContent: ReactNode; navContent: ReactNode;
footerContent: ReactNode; footerContent: ReactNode;
collapsed?: boolean;
onToggleCollapsed?: () => void;
collapseLabel?: string;
expandLabel?: string;
}; };
export function AppSidebar({ export function AppSidebar({
@@ -25,14 +30,57 @@ export function AppSidebar({
brandIcon, brandIcon,
navContent, navContent,
footerContent, footerContent,
collapsed = false,
onToggleCollapsed,
collapseLabel = "Collapse sidebar",
expandLabel = "Expand sidebar",
}: ShellSidebarProps) { }: ShellSidebarProps) {
return ( return (
<aside className={shellLayoutClasses.aside}> <aside
className={
collapsed ? shellLayoutClasses.asideCollapsed : shellLayoutClasses.aside
}
>
<div> <div>
<div className={shellLayoutClasses.brandSection}> <div
<div className={shellLayoutClasses.brandWrap}> className={
<div className={shellLayoutClasses.brandIcon}>{brandIcon}</div> collapsed
<div> ? shellLayoutClasses.brandSectionCollapsed
: shellLayoutClasses.brandSection
}
>
<div
className={
collapsed
? shellLayoutClasses.brandWrapCollapsed
: shellLayoutClasses.brandWrap
}
>
{onToggleCollapsed ? (
<button
type="button"
onClick={onToggleCollapsed}
className="grid h-11 w-11 place-items-center rounded-xl border border-border bg-primary/15 text-primary shadow-[0_12px_30px_rgba(54,211,153,0.22)] transition hover:bg-primary/20"
aria-label={collapsed ? expandLabel : collapseLabel}
title={collapsed ? expandLabel : collapseLabel}
>
<span className="sr-only">
{collapsed ? expandLabel : collapseLabel}
</span>
{collapsed ? <Menu size={20} /> : <SquareMenu size={20} />}
</button>
) : (
<div
className={
collapsed
? shellLayoutClasses.brandIconCollapsed
: shellLayoutClasses.brandIcon
}
>
{brandIcon}
</div>
)}
<div className={collapsed ? "hidden" : "block"}>
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground"> <p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">
{brandLabel} {brandLabel}
</p> </p>
@@ -40,7 +88,15 @@ export function AppSidebar({
</div> </div>
</div> </div>
</div> </div>
<nav className={shellLayoutClasses.navWrap}>{navContent}</nav> <nav
className={
collapsed
? shellLayoutClasses.navWrapCollapsed
: shellLayoutClasses.navWrap
}
>
{navContent}
</nav>
</div> </div>
<div>{footerContent}</div> <div>{footerContent}</div>

View File

@@ -27,6 +27,8 @@ type ShellProfileSummaryParams = {
export const SHELL_THEME_STORAGE_KEY = "admin_theme"; export const SHELL_THEME_STORAGE_KEY = "admin_theme";
export const SHELL_SESSION_EXPIRY_STORAGE_KEY = SESSION_EXPIRY_STORAGE_KEY; export const SHELL_SESSION_EXPIRY_STORAGE_KEY = SESSION_EXPIRY_STORAGE_KEY;
export const SHELL_SIDEBAR_COLLAPSED_STORAGE_KEY =
"baron_shell_sidebar_collapsed";
export type { ShellSidebarNavItem } from "./AppSidebar"; export type { ShellSidebarNavItem } from "./AppSidebar";
export { AppSidebar } from "./AppSidebar"; export { AppSidebar } from "./AppSidebar";
export { shellLayoutClasses } from "./layout"; export { shellLayoutClasses } from "./layout";
@@ -52,6 +54,25 @@ export function writeShellSessionExpiryEnabled(isEnabled: boolean) {
writeSessionExpiryEnabled(isEnabled); writeSessionExpiryEnabled(isEnabled);
} }
export function readShellSidebarCollapsed(defaultCollapsed = false) {
const stored = window.localStorage.getItem(
SHELL_SIDEBAR_COLLAPSED_STORAGE_KEY,
);
if (stored === null) {
return defaultCollapsed;
}
return stored === "true";
}
export function writeShellSidebarCollapsed(isCollapsed: boolean) {
window.localStorage.setItem(
SHELL_SIDEBAR_COLLAPSED_STORAGE_KEY,
String(isCollapsed),
);
}
export function buildShellProfileSummary({ export function buildShellProfileSummary({
profileName, profileName,
profileEmail, profileEmail,

View File

@@ -1,22 +1,34 @@
export const shellLayoutClasses = { export const shellLayoutClasses = {
root: "grid min-h-screen grid-cols-[240px,minmax(0,1fr)] bg-background text-foreground", root: "grid min-h-screen grid-cols-[240px,minmax(0,1fr)] bg-background text-foreground",
rootCollapsed:
"grid min-h-screen grid-cols-[80px,minmax(0,1fr)] bg-background text-foreground",
aside: aside:
"sticky top-0 flex h-screen flex-col justify-between border-r border-border bg-card backdrop-blur", "sticky top-0 flex h-screen flex-col justify-between border-r border-border bg-card backdrop-blur",
asideCollapsed:
"sticky top-0 flex h-screen flex-col justify-between border-r border-border bg-card backdrop-blur",
asideStatic: asideStatic:
"sticky top-0 h-screen border-r border-border bg-card backdrop-blur", "sticky top-0 h-screen border-r border-border bg-card backdrop-blur",
brandSection: brandSection:
"flex items-center justify-between px-5 py-4 md:block md:space-y-6 md:py-6", "flex items-center justify-between px-5 py-4 md:block md:space-y-6 md:py-6",
brandSectionCollapsed:
"flex items-center justify-between px-3 py-4 md:block md:space-y-4 md:px-2 md:py-6",
brandWrap: "flex items-center gap-3 md:flex-col md:items-start", brandWrap: "flex items-center gap-3 md:flex-col md:items-start",
brandWrapCollapsed: "flex items-center gap-3 md:flex-col md:items-center",
brandIcon: brandIcon:
"grid h-11 w-11 place-items-center rounded-xl bg-primary/15 text-primary shadow-[0_12px_30px_rgba(54,211,153,0.22)]", "grid h-11 w-11 place-items-center rounded-xl bg-primary/15 text-primary shadow-[0_12px_30px_rgba(54,211,153,0.22)]",
brandIconCollapsed:
"grid h-11 w-11 place-items-center rounded-xl bg-primary/15 text-primary shadow-[0_12px_30px_rgba(54,211,153,0.22)]",
scopeBadge: scopeBadge:
"hidden rounded-full border border-border px-3 py-2 text-xs text-muted-foreground md:inline-flex md:items-center md:gap-2", "hidden rounded-full border border-border px-3 py-2 text-xs text-muted-foreground md:inline-flex md:items-center md:gap-2",
navWrap: "px-2 pb-4 md:px-3 md:pb-8", navWrap: "px-2 pb-4 md:px-3 md:pb-8",
navWrapCollapsed: "px-2 pb-4 md:px-2 md:pb-8",
navMeta: navMeta:
"flex flex-wrap gap-2 px-3 pb-4 text-[11px] text-muted-foreground md:flex-col md:items-start", "flex flex-wrap gap-2 px-3 pb-4 text-[11px] text-muted-foreground md:flex-col md:items-start",
navList: "flex flex-col gap-1", navList: "flex flex-col gap-1",
navItemBase: navItemBase:
"flex items-center gap-3 rounded-xl px-3 py-3 text-sm transition", "flex items-center gap-3 rounded-xl px-3 py-3 text-sm transition",
navItemBaseCollapsed:
"flex items-center justify-center gap-0 rounded-xl px-3 py-3 text-sm transition",
navItemActive: navItemActive:
"bg-primary/10 text-primary shadow-[0_12px_40px_rgba(54,211,153,0.18)]", "bg-primary/10 text-primary shadow-[0_12px_40px_rgba(54,211,153,0.18)]",
navItemIdle: "text-muted-foreground hover:bg-muted/10 hover:text-foreground", navItemIdle: "text-muted-foreground hover:bg-muted/10 hover:text-foreground",
@@ -24,6 +36,8 @@ export const shellLayoutClasses = {
"hidden space-y-2 px-5 pb-6 pt-2 text-xs text-[var(--color-muted)] md:block", "hidden space-y-2 px-5 pb-6 pt-2 text-xs text-[var(--color-muted)] md:block",
logoutButton: logoutButton:
"flex w-full items-center gap-3 rounded-xl px-3 py-3 text-sm text-muted-foreground transition hover:bg-destructive/10 hover:text-destructive", "flex w-full items-center gap-3 rounded-xl px-3 py-3 text-sm text-muted-foreground transition hover:bg-destructive/10 hover:text-destructive",
logoutButtonCollapsed:
"flex w-full items-center justify-center gap-0 rounded-xl px-3 py-3 text-sm text-muted-foreground transition hover:bg-destructive/10 hover:text-destructive",
header: header:
"sticky top-0 z-20 border-b border-border bg-background/90 backdrop-blur", "sticky top-0 z-20 border-b border-border bg-background/90 backdrop-blur",
headerElevated: headerElevated:
@@ -31,8 +45,11 @@ export const shellLayoutClasses = {
headerInner: "flex items-center justify-between px-5 py-4 md:px-8", headerInner: "flex items-center justify-between px-5 py-4 md:px-8",
headerTitleWrap: "flex flex-col gap-1", headerTitleWrap: "flex flex-col gap-1",
headerActions: "flex items-center gap-2 text-sm", headerActions: "flex items-center gap-2 text-sm",
headerActionsCollapsed: "flex items-center gap-2 text-sm",
actionButton: actionButton:
"inline-flex items-center gap-2 rounded-full border border-border px-3 py-2 text-muted-foreground transition hover:bg-muted/20", "inline-flex items-center gap-2 rounded-full border border-border px-3 py-2 text-muted-foreground transition hover:bg-muted/20",
sidebarToggleButton:
"inline-flex items-center gap-2 rounded-full border border-border px-3 py-2 text-muted-foreground transition hover:bg-muted/20",
sessionBadge: sessionBadge:
"hidden rounded-full border px-3 py-2 text-xs font-medium md:inline-flex", "hidden rounded-full border px-3 py-2 text-xs font-medium md:inline-flex",
profileInitial: profileInitial:

View File

@@ -116,6 +116,24 @@ describe("devfront AppLayout", () => {
expect(document.documentElement.classList.contains("light")).toBe(true); expect(document.documentElement.classList.contains("light")).toBe(true);
}); });
it("toggles the sidebar and persists the collapsed state", async () => {
const container = await renderLayout();
const collapseButton = container.querySelector(
'button[aria-label="Collapse sidebar"]',
) as HTMLButtonElement;
await act(async () => {
collapseButton.click();
});
expect(window.localStorage.getItem("baron_shell_sidebar_collapsed")).toBe(
"true",
);
expect(
container.querySelector('button[aria-label="Expand sidebar"]'),
).not.toBeNull();
});
it("toggles profile menu, navigates to profile, toggles theme, and logs out", async () => { it("toggles profile menu, navigates to profile, toggles theme, and logs out", async () => {
const container = await renderLayout(); const container = await renderLayout();

View File

@@ -19,11 +19,13 @@ import {
buildShellProfileSummary, buildShellProfileSummary,
buildShellSessionStatus, buildShellSessionStatus,
readShellSessionExpiryEnabled, readShellSessionExpiryEnabled,
readShellSidebarCollapsed,
readShellTheme, readShellTheme,
type ShellSidebarNavItem, type ShellSidebarNavItem,
type ShellTranslator, type ShellTranslator,
shellLayoutClasses, shellLayoutClasses,
writeShellSessionExpiryEnabled, writeShellSessionExpiryEnabled,
writeShellSidebarCollapsed,
} from "../../../../common/shell"; } from "../../../../common/shell";
import { fetchMe } from "../../features/auth/authApi"; import { fetchMe } from "../../features/auth/authApi";
import { t } from "../../lib/i18n"; import { t } from "../../lib/i18n";
@@ -118,6 +120,9 @@ function AppLayout() {
const isDevelopmentRuntime = import.meta.env.MODE === "development"; const isDevelopmentRuntime = import.meta.env.MODE === "development";
const [theme, setTheme] = useState<"light" | "dark">(readShellTheme); const [theme, setTheme] = useState<"light" | "dark">(readShellTheme);
const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false); const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false);
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(() =>
readShellSidebarCollapsed(false),
);
const [, setDevelopmentRenderRevision] = useState(0); const [, setDevelopmentRenderRevision] = useState(0);
const [isSessionExpiryEnabled, setIsSessionExpiryEnabled] = useState(() => const [isSessionExpiryEnabled, setIsSessionExpiryEnabled] = useState(() =>
readShellSessionExpiryEnabled(!isDevelopmentRuntime), readShellSessionExpiryEnabled(!isDevelopmentRuntime),
@@ -352,26 +357,42 @@ function AppLayout() {
return next; return next;
}); });
}; };
const handleSidebarToggle = () => {
setIsSidebarCollapsed((prev) => {
const next = !prev;
writeShellSidebarCollapsed(next);
return next;
});
};
const sidebarNavContent = ( const sidebarNavContent = (
<div className={shellLayoutClasses.navList}> <div className={shellLayoutClasses.navList}>
{navItems.map(({ labelKey, labelFallback, to, icon: Icon }) => ( {navItems.map(({ labelKey, labelFallback, to, icon: Icon }) => {
<NavLink const label = t(labelKey, labelFallback);
key={to}
to={to} return (
end={to === "/"} <NavLink
className={({ isActive }) => key={to}
[ to={to}
shellLayoutClasses.navItemBase, end={to === "/"}
isActive className={({ isActive }) =>
? shellLayoutClasses.navItemActive [
: shellLayoutClasses.navItemIdle, shellLayoutClasses.navItemBase,
].join(" ") isSidebarCollapsed
} ? shellLayoutClasses.navItemBaseCollapsed
> : "",
<Icon size={18} /> isActive
<span>{t(labelKey, labelFallback)}</span> ? shellLayoutClasses.navItemActive
</NavLink> : shellLayoutClasses.navItemIdle,
))} ].join(" ")
}
title={label}
aria-label={label}
>
<Icon size={18} />
<span className={isSidebarCollapsed ? "sr-only" : ""}>{label}</span>
</NavLink>
);
})}
</div> </div>
); );
const sidebarFooterContent = ( const sidebarFooterContent = (
@@ -379,22 +400,39 @@ function AppLayout() {
<button <button
type="button" type="button"
onClick={handleLogout} onClick={handleLogout}
className={shellLayoutClasses.logoutButton} className={
isSidebarCollapsed
? shellLayoutClasses.logoutButtonCollapsed
: shellLayoutClasses.logoutButton
}
title={t("ui.shell.nav.logout", "Logout")}
> >
<LogOut size={18} /> <LogOut size={18} />
<span>{t("ui.shell.nav.logout", "Logout")}</span> <span className={isSidebarCollapsed ? "sr-only" : ""}>
{t("ui.shell.nav.logout", "Logout")}
</span>
</button> </button>
</div> </div>
); );
return ( return (
<div className={shellLayoutClasses.root}> <div
className={
isSidebarCollapsed
? shellLayoutClasses.rootCollapsed
: shellLayoutClasses.root
}
>
<AppSidebar <AppSidebar
brandLabel={t("ui.dev.brand", "Baron Sign In")} brandLabel={t("ui.dev.brand", "Baron Sign In")}
brandTitle={t("ui.dev.console_title", "Developer Console")} brandTitle={t("ui.dev.console_title", "Developer Console")}
brandIcon={<ShieldHalf size={20} />} brandIcon={<ShieldHalf size={20} />}
navContent={sidebarNavContent} navContent={sidebarNavContent}
footerContent={sidebarFooterContent} footerContent={sidebarFooterContent}
collapsed={isSidebarCollapsed}
onToggleCollapsed={handleSidebarToggle}
collapseLabel={t("ui.shell.sidebar.collapse", "사이드바 접기")}
expandLabel={t("ui.shell.sidebar.expand", "사이드바 펼치기")}
/> />
<div className={shellLayoutClasses.content}> <div className={shellLayoutClasses.content}>

View File

@@ -1361,6 +1361,10 @@ unknown_name = "Unknown User"
logout = "Logout" logout = "Logout"
profile = "My Profile" profile = "My Profile"
[ui.shell.sidebar]
collapse = "Collapse sidebar"
expand = "Expand sidebar"
[ui.shell.role] [ui.shell.role]
rp_admin = "Service Administrator (RP Admin)" rp_admin = "Service Administrator (RP Admin)"
super_admin = "System Administrator (Super Admin)" super_admin = "System Administrator (Super Admin)"

View File

@@ -1361,6 +1361,10 @@ unknown_name = "Unknown User"
logout = "Logout" logout = "Logout"
profile = "내 정보" profile = "내 정보"
[ui.shell.sidebar]
collapse = "사이드바 접기"
expand = "사이드바 펼치기"
[ui.shell.role] [ui.shell.role]
rp_admin = "서비스 관리자 (RP Admin)" rp_admin = "서비스 관리자 (RP Admin)"
super_admin = "시스템 관리자 (Super Admin)" super_admin = "시스템 관리자 (Super Admin)"

View File

@@ -1417,6 +1417,10 @@ unknown_name = ""
logout = "" logout = ""
profile = "" profile = ""
[ui.shell.sidebar]
collapse = ""
expand = ""
[ui.shell.role] [ui.shell.role]
rp_admin = "" rp_admin = ""
super_admin = "" super_admin = ""

View File

@@ -2601,6 +2601,10 @@ title_remote = "Sign-in Approved"
logout = "Logout" logout = "Logout"
profile = "My Profile" profile = "My Profile"
[ui.shell.sidebar]
collapse = "Collapse sidebar"
expand = "Expand sidebar"
[ui.shell.profile] [ui.shell.profile]
menu_aria = "Open account menu" menu_aria = "Open account menu"
menu_title = "Account" menu_title = "Account"

View File

@@ -3026,6 +3026,10 @@ title_remote = "로그인 승인 완료"
logout = "로그아웃" logout = "로그아웃"
profile = "내 정보" profile = "내 정보"
[ui.shell.sidebar]
collapse = "사이드바 접기"
expand = "사이드바 펼치기"
[ui.shell.profile] [ui.shell.profile]
menu_aria = "계정 메뉴 열기" menu_aria = "계정 메뉴 열기"
menu_title = "계정" menu_title = "계정"

View File

@@ -2904,6 +2904,10 @@ title_remote = ""
logout = "" logout = ""
profile = "" profile = ""
[ui.shell.sidebar]
collapse = ""
expand = ""
[ui.shell.profile] [ui.shell.profile]
menu_aria = "" menu_aria = ""
menu_title = "" menu_title = ""