1
0
forked from baron/baron-sso

adminfront ui 개편

This commit is contained in:
2026-03-20 10:58:52 +09:00
parent 918f133867
commit d0e4f8f86a
11 changed files with 142 additions and 298 deletions

View File

@@ -39,6 +39,18 @@ function AppLayout() {
return stored === "dark" ? "dark" : "light"; return stored === "dark" ? "dark" : "light";
}); });
const [isProfileOpen, setIsProfileOpen] = useState(false); const [isProfileOpen, setIsProfileOpen] = useState(false);
const [timeLeft, setTimeLeft] = useState<number | null>(null);
const expiresAt = auth.user?.expires_at;
useEffect(() => {
if (!expiresAt) return;
const updateTimer = () => {
setTimeLeft(Math.max(0, Math.floor(expiresAt - Date.now() / 1000)));
};
updateTimer();
const interval = setInterval(updateTimer, 1000);
return () => clearInterval(interval);
}, [expiresAt]);
const { data: profile } = useQuery({ const { data: profile } = useQuery({
queryKey: ["me"], queryKey: ["me"],
@@ -156,20 +168,8 @@ function AppLayout() {
</h1> </h1>
</div> </div>
</div> </div>
<div className="hidden rounded-full border border-border px-3 py-2 text-xs text-muted-foreground md:inline-flex md:items-center md:gap-2">
<BadgeCheck size={14} />
{t("msg.admin.scope_admin", "Scoped to /admin")}
</div>
</div> </div>
<nav className="px-2 pb-4 md:px-3 md:pb-8"> <nav className="px-2 pb-4 md:px-3 md:pb-8">
<div className="flex flex-wrap gap-2 px-3 pb-4 text-[11px] text-muted-foreground md:flex-col md:items-start">
<span className="rounded-full border border-border px-3 py-1">
{t("msg.admin.idp_env_prod", "IDP env: prod")}
</span>
<span className="rounded-full border border-border px-3 py-1">
{t("msg.admin.tenant_headers", "Tenant-aware headers")}
</span>
</div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
{navItems.map(({ label, to, icon: Icon }) => ( {navItems.map(({ label, to, icon: Icon }) => (
<NavLink <NavLink
@@ -201,36 +201,11 @@ function AppLayout() {
</button> </button>
</div> </div>
</nav> </nav>
<div className="hidden space-y-2 px-5 pb-6 text-xs text-[var(--color-muted)] md:block">
<p>
{t(
"msg.admin.notice.scope",
"관리 기능은 /admin 네임스페이스에서만 노출합니다.",
)}
</p>
<p>
{t(
"msg.admin.notice.idp_policy",
"IDP 관리 키는 서버 내부 래핑 API로만 사용하며, 감사·레이트리밋을 기본 적용합니다.",
)}
</p>
</div>
</aside> </aside>
<div className="relative"> <div className="relative">
<header className="sticky top-0 z-20 border-b border-border bg-background/90 backdrop-blur"> <header className="sticky top-0 z-50 border-b border-border bg-background/90 backdrop-blur">
<div className="flex items-center justify-between px-5 py-4 md:px-8"> <div className="flex items-center justify-end px-5 py-4 md:px-8">
<div className="flex flex-col gap-1">
<p className="text-xs uppercase tracking-[0.22em] text-muted-foreground">
{t("ui.admin.header.plane", "Admin Plane")}
</p>
<span className="text-lg font-semibold">
{t(
"msg.admin.header.subtitle",
"Tenant isolation & least privilege by default",
)}
</span>
</div>
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<LanguageSelector /> <LanguageSelector />
<button <button
@@ -266,7 +241,7 @@ function AppLayout() {
{isProfileOpen && ( {isProfileOpen && (
<> <>
<div <div
className="fixed inset-0 z-30" className="fixed inset-0 z-[90]"
onClick={() => setIsProfileOpen(false)} onClick={() => setIsProfileOpen(false)}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Escape") setIsProfileOpen(false); if (e.key === "Escape") setIsProfileOpen(false);
@@ -275,7 +250,7 @@ function AppLayout() {
tabIndex={-1} tabIndex={-1}
aria-label="Close profile menu" aria-label="Close profile menu"
/> />
<div className="absolute right-0 mt-2 w-56 origin-top-right rounded-xl border border-border bg-card p-2 shadow-xl ring-1 ring-black ring-opacity-5 focus:outline-none z-40 animate-in fade-in zoom-in-95 duration-200"> <div className="absolute right-0 mt-2 w-56 origin-top-right rounded-xl border border-border bg-card p-2 shadow-xl ring-1 ring-black ring-opacity-5 focus:outline-none z-[100] animate-in fade-in zoom-in-95 duration-200">
<div className="px-3 py-3 border-b border-border/50 mb-1"> <div className="px-3 py-3 border-b border-border/50 mb-1">
<p className="text-sm font-semibold truncate"> <p className="text-sm font-semibold truncate">
{profile?.name || auth.user?.profile.name} {profile?.name || auth.user?.profile.name}
@@ -364,8 +339,10 @@ function AppLayout() {
)} )}
</div> </div>
<span className="hidden md:inline-flex rounded-full border border-border px-3 py-2 text-muted-foreground"> <span className="hidden md:inline-flex rounded-full border border-border px-3 py-2 text-muted-foreground font-mono">
{t("msg.admin.session_ttl", "Session TTL: 15m admin")} {timeLeft !== null
? `Session TTL: ${Math.floor(timeLeft / 60)}m ${timeLeft % 60}s`
: t("msg.admin.session_ttl", "Session TTL: 15m admin")}
</span> </span>
</div> </div>
</div> </div>

View File

@@ -66,15 +66,6 @@ function ApiKeyListPage() {
<div className="space-y-6 flex flex-col h-[calc(100vh-theme(spacing.32))]"> <div className="space-y-6 flex flex-col h-[calc(100vh-theme(spacing.32))]">
<header className="flex flex-wrap items-start justify-between gap-4 flex-shrink-0 sticky top-[-2.5rem] z-20 bg-background/95 backdrop-blur pt-4 pb-2 -mt-4"> <header className="flex flex-wrap items-start justify-between gap-4 flex-shrink-0 sticky top-[-2.5rem] z-20 bg-background/95 backdrop-blur pt-4 pb-2 -mt-4">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2 text-sm text-[var(--color-muted)]">
<span>
{t("ui.admin.api_keys.list.breadcrumb.section", "API Keys")}
</span>
<span>/</span>
<span className="text-foreground">
{t("ui.admin.api_keys.list.breadcrumb.list", "List")}
</span>
</div>
<h2 className="text-3xl font-semibold"> <h2 className="text-3xl font-semibold">
{t("ui.admin.api_keys.list.title", "API 키 관리 (M2M)")} {t("ui.admin.api_keys.list.title", "API 키 관리 (M2M)")}
</h2> </h2>

View File

@@ -161,13 +161,6 @@ function AuditLogsPage() {
<div className="space-y-6 flex flex-col h-[calc(100vh-theme(spacing.32))]"> <div className="space-y-6 flex flex-col h-[calc(100vh-theme(spacing.32))]">
<header className="flex flex-wrap items-start justify-between gap-4 flex-shrink-0 sticky top-[-2.5rem] z-20 bg-background/95 backdrop-blur pt-4 pb-2 -mt-4"> <header className="flex flex-wrap items-start justify-between gap-4 flex-shrink-0 sticky top-[-2.5rem] z-20 bg-background/95 backdrop-blur pt-4 pb-2 -mt-4">
<div> <div>
<div className="flex items-center gap-2 text-sm text-[var(--color-muted)]">
<span>{t("ui.admin.audit.breadcrumb.section", "Audit")}</span>
<span>/</span>
<span className="text-foreground">
{t("ui.admin.audit.breadcrumb.logs", "Logs")}
</span>
</div>
<h2 className="text-3xl font-semibold"> <h2 className="text-3xl font-semibold">
{t("ui.admin.audit.title", "감사 로그")} {t("ui.admin.audit.title", "감사 로그")}
</h2> </h2>

View File

@@ -1,15 +1,14 @@
import { import {
Activity, Activity,
ArrowUpRight, ArrowUpRight,
Box,
Database, Database,
Key,
PlusCircle,
ShieldCheck, ShieldCheck,
Users, Users,
} from "lucide-react"; } from "lucide-react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { RoleGuard } from "../../components/auth/RoleGuard"; import { RoleGuard } from "../../components/auth/RoleGuard";
import { Badge } from "../../components/ui/badge";
import { Button } from "../../components/ui/button";
import { import {
Card, Card,
CardContent, CardContent,
@@ -22,244 +21,180 @@ import PermissionChecker from "./components/PermissionChecker";
function GlobalOverviewPage() { function GlobalOverviewPage() {
return ( return (
<div className="space-y-10"> <div className="space-y-8 animate-in fade-in duration-500">
<div className="flex flex-wrap items-start justify-between gap-4"> <div className="flex flex-wrap items-end justify-between gap-4">
<div className="space-y-2"> <div className="space-y-1">
<p className="text-xs uppercase tracking-[0.2em] text-[var(--color-muted)]"> <h2 className="text-3xl font-bold tracking-tight">
{t("ui.admin.overview.kicker", "Global Overview")} {t("ui.admin.overview.title", "Dashboard")}
</p>
<h2 className="text-3xl font-semibold">
{t("ui.admin.overview.title", "Tenant-independent control plane")}
</h2> </h2>
<p className="text-sm text-[var(--color-muted)]"> <p className="text-muted-foreground">
{t( {t(
"msg.admin.overview.description", "msg.admin.overview.description",
"모든 테넌트 공통 지표와 정책 상태를 한 곳에서 확인합니다.", "시스템 전반의 주요 현황을 확인하고 관리합니다.",
)} )}
</p> </p>
</div> </div>
<RoleGuard roles={["super_admin"]}>
<div className="flex items-center gap-2">
<Badge variant="muted">
{t("msg.admin.overview.idp_primary", "IDP: Ory primary")}
</Badge>
<Badge variant="muted">
{t("msg.admin.overview.idp_fallback", "Fallback: Descope")}
</Badge>
</div>
</RoleGuard>
</div> </div>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4"> <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<RoleGuard roles={["super_admin"]}> <RoleGuard roles={["super_admin"]}>
<Card className="bg-[var(--color-panel)]"> <Card className="transition-all hover:shadow-md">
<CardHeader className="flex flex-row items-center justify-between pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardDescription> <CardTitle className="text-sm font-medium">
{t("ui.admin.overview.summary.total_tenants", "Total Tenants")} {t("ui.admin.overview.summary.total_tenants", "총 테넌트")}
</CardDescription> </CardTitle>
<div className="rounded-full border border-[var(--color-border)] p-2 text-[var(--color-muted)]"> <div className="rounded-full bg-primary/10 p-2 text-primary">
<Users size={16} /> <Users size={16} />
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-semibold">-</div> <div className="text-2xl font-bold">-</div>
<p className="mt-1 text-xs text-[var(--color-muted)]"> <p className="mt-1 text-xs text-muted-foreground">
{t(
"msg.admin.overview.summary.total_tenants",
"Tenant-aware core",
)}
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-[var(--color-panel)]"> <Card className="transition-all hover:shadow-md">
<CardHeader className="flex flex-row items-center justify-between pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardDescription> <CardTitle className="text-sm font-medium">
{t("ui.admin.overview.summary.oidc_clients", "OIDC Clients")} {t("ui.admin.overview.summary.oidc_clients", "연동 클라이언트")}
</CardDescription> </CardTitle>
<div className="rounded-full border border-[var(--color-border)] p-2 text-[var(--color-muted)]"> <div className="rounded-full bg-blue-500/10 p-2 text-blue-500">
<ShieldCheck size={16} /> <ShieldCheck size={16} />
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-semibold">-</div> <div className="text-2xl font-bold">-</div>
<p className="mt-1 text-xs text-[var(--color-muted)]"> <p className="mt-1 text-xs text-muted-foreground">
{t("msg.admin.overview.summary.oidc_clients", "Hydra registry")} OIDC
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
</RoleGuard> </RoleGuard>
<Card className="bg-[var(--color-panel)]"> <Card className="transition-all hover:shadow-md">
<CardHeader className="flex flex-row items-center justify-between pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardDescription> <CardTitle className="text-sm font-medium">
{t( {t(
"ui.admin.overview.summary.audit_events_24h", "ui.admin.overview.summary.audit_events_24h",
"Audit Events (24h)", "최근 감사 로그 (24h)",
)} )}
</CardDescription> </CardTitle>
<div className="rounded-full border border-[var(--color-border)] p-2 text-[var(--color-muted)]"> <div className="rounded-full bg-orange-500/10 p-2 text-orange-500">
<Activity size={16} /> <Activity size={16} />
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-semibold">-</div> <div className="text-2xl font-bold">-</div>
<p className="mt-1 text-xs text-[var(--color-muted)]"> <p className="mt-1 text-xs text-muted-foreground">
{t(
"msg.admin.overview.summary.audit_events_24h",
"ClickHouse stream",
)}
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-[var(--color-panel)]"> <Card className="transition-all hover:shadow-md">
<CardHeader className="flex flex-row items-center justify-between pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardDescription> <CardTitle className="text-sm font-medium">
{t("ui.admin.overview.summary.policy_gate", "Policy Gate")} {t("ui.admin.overview.summary.policy_gate", "정책 상태")}
</CardDescription> </CardTitle>
<div className="rounded-full border border-[var(--color-border)] p-2 text-[var(--color-muted)]"> <div className="rounded-full bg-green-500/10 p-2 text-green-500">
<Database size={16} /> <Database size={16} />
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-semibold">Planned</div> <div className="text-2xl font-bold text-green-600 dark:text-green-500">
<p className="mt-1 text-xs text-[var(--color-muted)]"> Active
{t( </div>
"msg.admin.overview.summary.policy_gate", <p className="mt-1 text-xs text-muted-foreground">
"Keto + Admin checks",
)}
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
<div className="grid gap-6 lg:grid-cols-[1.4fr,1fr]"> <div className="space-y-4">
<Card className="bg-[var(--color-panel)]"> <h3 className="text-lg font-semibold tracking-tight">
<CardHeader> {t("ui.admin.overview.quick_links.title", "빠른 작업")}
<CardTitle className="text-xl"> </h3>
{t("ui.admin.overview.playbook.title", "Admin playbook")} <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
</CardTitle> <RoleGuard roles={["super_admin"]}>
<CardDescription> <Link
{t( to="/tenants/new"
"msg.admin.overview.playbook.description", className="group flex flex-col justify-between rounded-xl border border-border bg-card p-5 shadow-sm transition-all hover:border-primary/50 hover:shadow-md"
"운영 정책, 레이트리밋, 감사 로그의 기본 룰을 요약합니다.", >
)} <div className="mb-4 inline-flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10 text-primary transition-colors group-hover:bg-primary group-hover:text-primary-foreground">
</CardDescription> <PlusCircle size={20} />
</CardHeader>
<CardContent className="space-y-3 text-sm text-[var(--color-muted)]">
<div className="flex items-start gap-3">
<div className="mt-1 rounded-full border border-[var(--color-border)] p-2">
<ShieldCheck size={14} />
</div> </div>
<div> <div>
<p className="font-semibold text-foreground"> <h4 className="font-semibold transition-colors group-hover:text-primary">
{t(
"msg.admin.overview.playbook.idp_title", </h4>
"Backend-only IDP access", <p className="mt-1 text-xs text-muted-foreground">
)} .
</p>
<p>
{t(
"msg.admin.overview.playbook.idp_body",
"모든 IDP 호출은 backend를 통해서만 수행하며, Hydra/Kratos admin 포트는 외부에 노출하지 않습니다.",
)}
</p> </p>
</div> </div>
</div> </Link>
<div className="flex items-start gap-3"> </RoleGuard>
<div className="mt-1 rounded-full border border-[var(--color-border)] p-2">
<Box size={14} />
</div>
<div>
<p className="font-semibold text-foreground">
{t(
"msg.admin.overview.playbook.tenant_title",
"Tenant isolation",
)}
</p>
<p>
{t(
"msg.admin.overview.playbook.tenant_body",
"Tenant 헤더와 감사 로그 규칙을 기본 적용하며, 향후 Keto 정책으로 확장 예정입니다.",
)}
</p>
</div>
</div>
</CardContent>
</Card>
<Card className="bg-[var(--color-panel)]"> <Link
<CardHeader> to="/users"
<CardTitle className="text-xl"> className="group flex flex-col justify-between rounded-xl border border-border bg-card p-5 shadow-sm transition-all hover:border-blue-500/50 hover:shadow-md"
{t("ui.admin.overview.quick_links.title", "빠른 이동")} >
</CardTitle> <div className="mb-4 inline-flex h-10 w-10 items-center justify-center rounded-lg bg-blue-500/10 text-blue-500 transition-colors group-hover:bg-blue-500 group-hover:text-white">
<CardDescription> <Users size={20} />
{t( </div>
"msg.admin.overview.quick_links.description", <div>
"주요 운영 화면으로 바로 이동합니다.", <h4 className="font-semibold transition-colors group-hover:text-blue-500">
)}
</CardDescription> </h4>
</CardHeader> <p className="mt-1 text-xs text-muted-foreground">
<CardContent className="space-y-3"> .
<RoleGuard roles={["super_admin"]}> </p>
<Button </div>
asChild </Link>
className="w-full justify-between"
variant="outline" <RoleGuard roles={["super_admin"]}>
> <Link
<Link to="/tenants/new"> to="/api-keys"
{t("ui.admin.overview.quick_links.add_tenant", "테넌트 추가")} className="group flex flex-col justify-between rounded-xl border border-border bg-card p-5 shadow-sm transition-all hover:border-purple-500/50 hover:shadow-md"
<ArrowUpRight size={16} />
</Link>
</Button>
</RoleGuard>
<Button
asChild
className="w-full justify-between"
variant="outline"
> >
<Link to="/users"> <div className="mb-4 inline-flex h-10 w-10 items-center justify-center rounded-lg bg-purple-500/10 text-purple-500 transition-colors group-hover:bg-purple-500 group-hover:text-white">
{t( <Key size={20} />
"ui.admin.overview.quick_links.user_management", </div>
"사용자 관리", <div>
)} <h4 className="font-semibold transition-colors group-hover:text-purple-500">
<ArrowUpRight size={16} /> API
</Link> </h4>
</Button> <p className="mt-1 text-xs text-muted-foreground">
<RoleGuard roles={["super_admin"]}> .
<Button </p>
asChild </div>
className="w-full justify-between" </Link>
variant="outline" </RoleGuard>
>
<Link to="/api-keys"> <Link
{t( to="/audit-logs"
"ui.admin.overview.quick_links.api_key_management", className="group flex flex-col justify-between rounded-xl border border-border bg-card p-5 shadow-sm transition-all hover:border-orange-500/50 hover:shadow-md"
"API 키 관리", >
)} <div className="mb-4 inline-flex h-10 w-10 items-center justify-center rounded-lg bg-orange-500/10 text-orange-500 transition-colors group-hover:bg-orange-500 group-hover:text-white">
<ArrowUpRight size={16} /> <Activity size={20} />
</Link> </div>
</Button> <div>
</RoleGuard> <h4 className="font-semibold transition-colors group-hover:text-orange-500">
<Button
asChild </h4>
className="w-full justify-between" <p className="mt-1 text-xs text-muted-foreground">
variant="outline" .
> </p>
<Link to="/audit-logs"> </div>
{t( </Link>
"ui.admin.overview.quick_links.view_audit_logs", </div>
"감사 로그 보기",
)}
<ArrowUpRight size={16} />
</Link>
</Button>
</CardContent>
</Card>
</div> </div>
<RoleGuard roles={["super_admin"]}> <RoleGuard roles={["super_admin"]}>
<PermissionChecker /> <div className="pt-4">
<PermissionChecker />
</div>
</RoleGuard> </RoleGuard>
</div> </div>
); );

View File

@@ -58,15 +58,6 @@ function TenantCreatePage() {
return ( return (
<div className="space-y-8"> <div className="space-y-8">
<header className="space-y-2"> <header className="space-y-2">
<div className="flex items-center gap-2 text-sm text-[var(--color-muted)]">
<span>
{t("ui.admin.tenants.create.breadcrumb.section", "Tenants")}
</span>
<span>/</span>
<span className="text-foreground">
{t("ui.admin.tenants.create.breadcrumb.action", "Create")}
</span>
</div>
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-between gap-3">
<div> <div>
<h2 className="text-3xl font-semibold"> <h2 className="text-3xl font-semibold">

View File

@@ -32,19 +32,6 @@ function TenantDetailPage() {
<div className="space-y-8"> <div className="space-y-8">
<header className="flex flex-wrap items-start justify-between gap-4"> <header className="flex flex-wrap items-start justify-between gap-4">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2 text-sm text-[var(--color-muted)]">
<Link
to="/tenants"
className="inline-flex items-center gap-2 hover:text-foreground transition-colors"
>
<ArrowLeft size={14} />
{t("ui.admin.tenants.detail.breadcrumb_list", "테넌트 목록")}
</Link>
<span>/</span>
<span className="text-foreground">
{t("ui.admin.tenants.detail.title", "상세")}
</span>
</div>
<h2 className="text-3xl font-semibold"> <h2 className="text-3xl font-semibold">
{tenantQuery.data?.name ?? {tenantQuery.data?.name ??
t("ui.admin.tenants.detail.loading", "불러오는 중...")} t("ui.admin.tenants.detail.loading", "불러오는 중...")}

View File

@@ -119,13 +119,6 @@ function TenantListPage() {
<div className="space-y-6 flex flex-col h-[calc(100vh-theme(spacing.32))]"> <div className="space-y-6 flex flex-col h-[calc(100vh-theme(spacing.32))]">
<header className="flex flex-wrap items-start justify-between gap-4 flex-shrink-0 sticky top-[-2.5rem] z-20 bg-background/95 backdrop-blur pt-4 pb-2 -mt-4"> <header className="flex flex-wrap items-start justify-between gap-4 flex-shrink-0 sticky top-[-2.5rem] z-20 bg-background/95 backdrop-blur pt-4 pb-2 -mt-4">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2 text-sm text-[var(--color-muted)]">
<span>{t("ui.admin.tenants.breadcrumb.section", "Tenants")}</span>
<span>/</span>
<span className="text-foreground">
{t("ui.admin.tenants.breadcrumb.list", "List")}
</span>
</div>
<h2 className="text-3xl font-semibold"> <h2 className="text-3xl font-semibold">
{t("ui.admin.tenants.title", "테넌트 목록")} {t("ui.admin.tenants.title", "테넌트 목록")}
</h2> </h2>

View File

@@ -177,15 +177,6 @@ function UserCreatePage() {
<div className="max-w-3xl space-y-8"> <div className="max-w-3xl space-y-8">
<header className="flex flex-wrap items-center justify-between gap-4"> <header className="flex flex-wrap items-center justify-between gap-4">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2 text-sm text-[var(--color-muted)]">
<Link to="/users" className="hover:underline">
{t("ui.admin.users.create.breadcrumb.section", "Users")}
</Link>
<span>/</span>
<span className="text-foreground">
{t("ui.admin.users.create.breadcrumb.new", "New")}
</span>
</div>
<h2 className="text-3xl font-semibold"> <h2 className="text-3xl font-semibold">
{t("ui.admin.users.create.title", "사용자 추가")} {t("ui.admin.users.create.title", "사용자 추가")}
</h2> </h2>

View File

@@ -315,13 +315,6 @@ function UserDetailPage() {
<div className="max-w-3xl space-y-8"> <div className="max-w-3xl space-y-8">
<header className="flex flex-wrap items-center justify-between gap-4"> <header className="flex flex-wrap items-center justify-between gap-4">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2 text-sm text-[var(--color-muted)]">
<Link to="/users" className="hover:underline">
{t("ui.admin.users.detail.breadcrumb.section", "Users")}
</Link>
<span>/</span>
<span className="text-foreground">{user.name}</span>
</div>
<h2 className="text-3xl font-semibold"> <h2 className="text-3xl font-semibold">
{t("ui.admin.users.detail.title", "사용자 상세")} {t("ui.admin.users.detail.title", "사용자 상세")}
</h2> </h2>

View File

@@ -257,13 +257,6 @@ function UserListPage() {
<div className="space-y-6 flex flex-col h-[calc(100vh-theme(spacing.32))]"> <div className="space-y-6 flex flex-col h-[calc(100vh-theme(spacing.32))]">
<header className="flex flex-wrap items-start justify-between gap-4 flex-shrink-0 sticky top-[-2.5rem] z-20 bg-background/95 backdrop-blur pt-4 pb-2 -mt-4"> <header className="flex flex-wrap items-start justify-between gap-4 flex-shrink-0 sticky top-[-2.5rem] z-20 bg-background/95 backdrop-blur pt-4 pb-2 -mt-4">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2 text-sm text-[var(--color-muted)]">
<span>{t("ui.admin.users.list.breadcrumb.section", "Users")}</span>
<span>/</span>
<span className="text-foreground">
{t("ui.admin.users.list.breadcrumb.list", "List")}
</span>
</div>
<h2 className="text-3xl font-semibold" data-testid="page-title"> <h2 className="text-3xl font-semibold" data-testid="page-title">
{t("ui.admin.users.list.title", "사용자 관리")} {t("ui.admin.users.list.title", "사용자 관리")}
</h2> </h2>

View File

@@ -880,7 +880,7 @@ start_import = "임포트 시작"
[ui.admin.overview] [ui.admin.overview]
kicker = "Global Overview" kicker = "Global Overview"
title = "Tenant-independent control plane" title = "통합 대시보드"
[ui.admin.overview.playbook] [ui.admin.overview.playbook]
title = "Admin playbook" title = "Admin playbook"