forked from baron/baron-sso
박스 타이틀 description 추가 및 로고 추가
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -19,6 +19,7 @@ reports
|
|||||||
reports/*
|
reports/*
|
||||||
config/*.pem
|
config/*.pem
|
||||||
common/node_modules
|
common/node_modules
|
||||||
|
common/.baron-deps-install.lock
|
||||||
|
|
||||||
# Docker Services Data (Volumes)
|
# Docker Services Data (Volumes)
|
||||||
postgres_data/
|
postgres_data/
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ function ApiKeyListPage() {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
sticky
|
sticky
|
||||||
titleAs="h2"
|
titleAs="h2"
|
||||||
|
icon={<Key size={20} />}
|
||||||
title={t("ui.admin.api_keys.list.title", "API 키 관리 (M2M)")}
|
title={t("ui.admin.api_keys.list.title", "API 키 관리 (M2M)")}
|
||||||
description={t(
|
description={t(
|
||||||
"msg.admin.api_keys.list.subtitle",
|
"msg.admin.api_keys.list.subtitle",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
import type { AxiosError } from "axios";
|
import type { AxiosError } from "axios";
|
||||||
import { Download, RefreshCw, Search } from "lucide-react";
|
import { Download, NotebookTabs, RefreshCw, Search } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {
|
import {
|
||||||
formatAuditValue,
|
formatAuditValue,
|
||||||
@@ -16,6 +16,7 @@ import { Button } from "../../components/ui/button";
|
|||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "../../components/ui/card";
|
} from "../../components/ui/card";
|
||||||
@@ -99,6 +100,7 @@ function AuditLogsPage() {
|
|||||||
"msg.admin.audit.subtitle",
|
"msg.admin.audit.subtitle",
|
||||||
"관리자 작업 이력을 조회합니다.",
|
"관리자 작업 이력을 조회합니다.",
|
||||||
)}
|
)}
|
||||||
|
icon={<NotebookTabs size={20} />}
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<Badge variant="muted">
|
<Badge variant="muted">
|
||||||
@@ -128,6 +130,12 @@ function AuditLogsPage() {
|
|||||||
<CardTitle className="text-lg font-bold flex items-center gap-2">
|
<CardTitle className="text-lg font-bold flex items-center gap-2">
|
||||||
{t("ui.common.audit.registry.title", "Audit registry")}
|
{t("ui.common.audit.registry.title", "Audit registry")}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{t(
|
||||||
|
"msg.admin.audit.registry.description",
|
||||||
|
"최근 감사 로그를 검색 조건에 맞춰 필터링하고, 작업 이력을 빠르게 확인합니다.",
|
||||||
|
)}
|
||||||
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4 pt-0">
|
<CardContent className="space-y-4 pt-0">
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ShieldHalf } from "lucide-react";
|
||||||
import { t } from "../../lib/i18n";
|
import { t } from "../../lib/i18n";
|
||||||
import PermissionChecker from "./components/PermissionChecker";
|
import PermissionChecker from "./components/PermissionChecker";
|
||||||
|
|
||||||
@@ -6,8 +7,11 @@ function AuthPage() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h2 className="text-3xl font-semibold">
|
<h2 className="flex items-center gap-2 text-3xl font-semibold">
|
||||||
{t("ui.admin.auth_guard.title", "Auth Guard")}
|
<span className="flex h-10 w-10 items-center justify-center rounded-xl border border-primary/15 bg-primary/10 text-primary">
|
||||||
|
<ShieldHalf size={20} />
|
||||||
|
</span>
|
||||||
|
<span>{t("ui.admin.auth_guard.title", "Auth Guard")}</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{t(
|
{t(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { CheckCircle2, XCircle } from "lucide-react";
|
import { CheckCircle2, ShieldCheck, XCircle } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button } from "../../../components/ui/button";
|
import { Button } from "../../../components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -47,7 +47,10 @@ function PermissionChecker() {
|
|||||||
return (
|
return (
|
||||||
<Card className="border-primary/20 bg-[var(--color-panel)]">
|
<Card className="border-primary/20 bg-[var(--color-panel)]">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-lg font-bold flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2 text-lg font-bold">
|
||||||
|
<span className="flex h-8 w-8 items-center justify-center rounded-lg border border-primary/15 bg-primary/10 text-primary">
|
||||||
|
<ShieldCheck size={16} />
|
||||||
|
</span>
|
||||||
{t(
|
{t(
|
||||||
"ui.admin.auth_guard.checker.title",
|
"ui.admin.auth_guard.checker.title",
|
||||||
"ReBAC permission checker",
|
"ReBAC permission checker",
|
||||||
|
|||||||
@@ -90,6 +90,23 @@ function integritySectionLabel(key: string, fallback: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function integritySectionDescription(key: string) {
|
||||||
|
switch (key) {
|
||||||
|
case "tenant_integrity":
|
||||||
|
return t(
|
||||||
|
"msg.admin.integrity.section.tenant_integrity.description",
|
||||||
|
"테넌트 slug 중복과 부모 관계 이상을 확인합니다.",
|
||||||
|
);
|
||||||
|
case "user_integrity":
|
||||||
|
return t(
|
||||||
|
"msg.admin.integrity.section.user_integrity.description",
|
||||||
|
"사용자와 로그인 ID 참조의 고아 레코드를 확인합니다.",
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function integrityCheckLabel(key: string, fallback: string) {
|
function integrityCheckLabel(key: string, fallback: string) {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "duplicate_tenant_slugs":
|
case "duplicate_tenant_slugs":
|
||||||
@@ -327,6 +344,10 @@ function DataIntegrityContent() {
|
|||||||
return (
|
return (
|
||||||
<main className="space-y-6">
|
<main className="space-y-6">
|
||||||
<header className="flex flex-shrink-0 flex-wrap items-start justify-between gap-4 sticky top-[-2.5rem] z-20 -mt-4 bg-background/95 pb-2 pt-4 backdrop-blur">
|
<header className="flex flex-shrink-0 flex-wrap items-start justify-between gap-4 sticky top-[-2.5rem] z-20 -mt-4 bg-background/95 pb-2 pt-4 backdrop-blur">
|
||||||
|
<div className="flex min-w-0 items-start gap-3">
|
||||||
|
<div className="mt-1 flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-primary/15 bg-primary/10 text-primary">
|
||||||
|
<Database size={20} />
|
||||||
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h2 className="text-3xl font-semibold">
|
<h2 className="text-3xl font-semibold">
|
||||||
{t("ui.admin.integrity.title", "데이터 정합성 검증")}
|
{t("ui.admin.integrity.title", "데이터 정합성 검증")}
|
||||||
@@ -338,6 +359,7 @@ function DataIntegrityContent() {
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col items-end gap-1">
|
<div className="flex flex-col items-end gap-1">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -444,9 +466,14 @@ function DataIntegrityContent() {
|
|||||||
className="rounded-lg border border-border bg-card p-5"
|
className="rounded-lg border border-border bg-card p-5"
|
||||||
>
|
>
|
||||||
<div className="mb-4 flex items-center justify-between gap-3">
|
<div className="mb-4 flex items-center justify-between gap-3">
|
||||||
|
<div className="space-y-1">
|
||||||
<h3 className="text-lg font-bold flex items-center gap-2">
|
<h3 className="text-lg font-bold flex items-center gap-2">
|
||||||
{integritySectionLabel(section.key, section.label)}
|
{integritySectionLabel(section.key, section.label)}
|
||||||
</h3>
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{integritySectionDescription(section.key)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<Badge variant={statusBadgeVariant(section.status)}>
|
<Badge variant={statusBadgeVariant(section.status)}>
|
||||||
{statusLabel(section.status)}
|
{statusLabel(section.status)}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Database,
|
Database,
|
||||||
|
LayoutDashboard,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
Users,
|
Users,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@@ -511,6 +512,10 @@ function GlobalOverviewPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-4 animate-in fade-in duration-500">
|
<div className="space-y-4 animate-in fade-in duration-500">
|
||||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||||
|
<div className="flex min-w-0 items-start gap-3">
|
||||||
|
<div className="mt-1 flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-primary/15 bg-primary/10 text-primary">
|
||||||
|
<LayoutDashboard size={20} />
|
||||||
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h2 className="text-3xl font-semibold">
|
<h2 className="text-3xl font-semibold">
|
||||||
{t("ui.common.overview.title", "운영 현황")}
|
{t("ui.common.overview.title", "운영 현황")}
|
||||||
@@ -523,6 +528,7 @@ function GlobalOverviewPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 border-y border-border/60 py-2">
|
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 border-y border-border/60 py-2">
|
||||||
<RoleGuard roles={["super_admin"]}>
|
<RoleGuard roles={["super_admin"]}>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { AlertTriangle, RefreshCw, RotateCcw } from "lucide-react";
|
import { AlertTriangle, RefreshCw, RotateCcw, Users } from "lucide-react";
|
||||||
import { RoleGuard } from "../../components/auth/RoleGuard";
|
import { RoleGuard } from "../../components/auth/RoleGuard";
|
||||||
import { Badge } from "../../components/ui/badge";
|
import { Badge } from "../../components/ui/badge";
|
||||||
import { Button } from "../../components/ui/button";
|
import { Button } from "../../components/ui/button";
|
||||||
@@ -97,6 +97,10 @@ function UserProjectionContent() {
|
|||||||
return (
|
return (
|
||||||
<main className="space-y-6 flex flex-col h-[calc(100vh-theme(spacing.32))]">
|
<main className="space-y-6 flex flex-col h-[calc(100vh-theme(spacing.32))]">
|
||||||
<header className="flex flex-shrink-0 flex-wrap items-start justify-between gap-4 sticky top-[-2.5rem] z-20 -mt-4 bg-background/95 pb-2 pt-4 backdrop-blur">
|
<header className="flex flex-shrink-0 flex-wrap items-start justify-between gap-4 sticky top-[-2.5rem] z-20 -mt-4 bg-background/95 pb-2 pt-4 backdrop-blur">
|
||||||
|
<div className="flex min-w-0 items-start gap-3">
|
||||||
|
<div className="mt-1 flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-primary/15 bg-primary/10 text-primary">
|
||||||
|
<Users size={20} />
|
||||||
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h2 className="text-3xl font-semibold">
|
<h2 className="text-3xl font-semibold">
|
||||||
{t(
|
{t(
|
||||||
@@ -111,6 +115,7 @@ function UserProjectionContent() {
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
sortItems,
|
sortItems,
|
||||||
toggleSort,
|
toggleSort,
|
||||||
} from "../../../../../common/core/utils";
|
} from "../../../../../common/core/utils";
|
||||||
|
import { PageHeader } from "../../../../../common/core/components/page";
|
||||||
import {
|
import {
|
||||||
commonStickyTableHeaderClass,
|
commonStickyTableHeaderClass,
|
||||||
commonTableShellClass,
|
commonTableShellClass,
|
||||||
@@ -745,85 +746,30 @@ function TenantListPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<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">
|
<PageHeader
|
||||||
<div className="space-y-2">
|
sticky
|
||||||
<h2 className="text-3xl font-semibold">
|
titleAs="h2"
|
||||||
{t("ui.admin.tenants.title", "테넌트 목록")}
|
icon={<Building2 size={20} />}
|
||||||
</h2>
|
title={t("ui.admin.tenants.title", "테넌트 목록")}
|
||||||
<p className="text-sm text-[var(--color-muted)]">
|
description={t(
|
||||||
{t(
|
|
||||||
"msg.admin.tenants.subtitle",
|
"msg.admin.tenants.subtitle",
|
||||||
"시스템에 등록된 모든 테넌트를 평면 목록으로 확인하고 관리합니다.",
|
"시스템에 등록된 모든 테넌트를 평면 목록으로 확인하고 관리합니다.",
|
||||||
)}
|
)}
|
||||||
</p>
|
actions={
|
||||||
</div>
|
<>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="relative w-64 mr-2">
|
<div className="relative mr-2 w-64">
|
||||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
placeholder={t(
|
placeholder={t(
|
||||||
"ui.admin.tenants.list.search_placeholder",
|
"ui.admin.tenants.list.search_placeholder",
|
||||||
"테넌트 이름, 슬러그, UUID 검색...",
|
"테넌트 이름 또는 슬러그 검색...",
|
||||||
)}
|
)}
|
||||||
className="pl-9 h-9"
|
className="h-9 pl-9"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
className="flex rounded-md border bg-background p-0.5"
|
|
||||||
data-testid="tenant-view-mode-toggle"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant={viewMode === "tree" ? "default" : "ghost"}
|
|
||||||
size="sm"
|
|
||||||
className="h-8 gap-1.5"
|
|
||||||
onClick={() => setViewMode("tree")}
|
|
||||||
data-testid="tenant-view-tree-btn"
|
|
||||||
>
|
|
||||||
<Network size={14} />
|
|
||||||
{t("ui.admin.tenants.view.tree", "트리")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant={viewMode === "table" ? "default" : "ghost"}
|
|
||||||
size="sm"
|
|
||||||
className="h-8 gap-1.5"
|
|
||||||
onClick={() => setViewMode("table")}
|
|
||||||
data-testid="tenant-view-table-btn"
|
|
||||||
>
|
|
||||||
<List size={14} />
|
|
||||||
{t("ui.admin.tenants.view.table", "평면")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant={scopeTenantId ? "default" : "outline"}
|
|
||||||
size="sm"
|
|
||||||
className="h-9 gap-2"
|
|
||||||
onClick={() => setScopePickerOpen(true)}
|
|
||||||
data-testid="tenant-scope-picker-btn"
|
|
||||||
>
|
|
||||||
<Network size={16} />
|
|
||||||
{selectedScopeTenant
|
|
||||||
? t("ui.admin.tenants.scope.active", "{{name}} 하위", {
|
|
||||||
name: selectedScopeTenant.name,
|
|
||||||
})
|
|
||||||
: t("ui.admin.tenants.scope.pick", "상위 범위 선택")}
|
|
||||||
</Button>
|
|
||||||
{scopeTenantId && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-9"
|
|
||||||
onClick={() => setScopeTenantId("")}
|
|
||||||
data-testid="tenant-scope-clear-btn"
|
|
||||||
>
|
|
||||||
{t("ui.common.clear", "초기화")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<RoleGuard roles={["super_admin"]}>
|
<RoleGuard roles={["super_admin"]}>
|
||||||
<input
|
<input
|
||||||
@@ -885,7 +831,10 @@ function TenantListPage() {
|
|||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
>
|
>
|
||||||
<Download size={16} className="mr-2 opacity-50" />
|
<Download size={16} className="mr-2 opacity-50" />
|
||||||
{t("ui.admin.tenants.export_with_ids", "UUID 포함 내보내기")}
|
{t(
|
||||||
|
"ui.admin.tenants.export_with_ids",
|
||||||
|
"UUID 포함 내보내기",
|
||||||
|
)}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
@@ -912,15 +861,17 @@ function TenantListPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</RoleGuard>
|
</RoleGuard>
|
||||||
</div>
|
</div>
|
||||||
{importMessage && (
|
{importMessage ? (
|
||||||
<div
|
<div
|
||||||
className="basis-full rounded-md border border-border bg-secondary px-3 py-2 text-sm"
|
className="rounded-md border border-border bg-secondary px-3 py-2 text-sm"
|
||||||
data-testid="tenant-import-result"
|
data-testid="tenant-import-result"
|
||||||
>
|
>
|
||||||
{importMessage}
|
{importMessage}
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : null}
|
||||||
</header>
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<Card className="bg-[var(--color-panel)] flex-1 flex flex-col min-h-0 overflow-hidden">
|
<Card className="bg-[var(--color-panel)] flex-1 flex flex-col min-h-0 overflow-hidden">
|
||||||
<CardHeader className="flex flex-row items-center justify-between flex-shrink-0">
|
<CardHeader className="flex flex-row items-center justify-between flex-shrink-0">
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
|
Users,
|
||||||
FileDown,
|
FileDown,
|
||||||
LayoutDashboard,
|
|
||||||
Plus,
|
Plus,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Search,
|
Search,
|
||||||
@@ -416,6 +416,7 @@ function UserListPage() {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
sticky
|
sticky
|
||||||
titleAs="h2"
|
titleAs="h2"
|
||||||
|
icon={<Users size={20} />}
|
||||||
title={
|
title={
|
||||||
<span data-testid="page-title">
|
<span data-testid="page-title">
|
||||||
{t("ui.admin.users.list.title", "사용자 관리")}
|
{t("ui.admin.users.list.title", "사용자 관리")}
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ empty = "Empty"
|
|||||||
|
|
||||||
[msg.admin.audit.registry]
|
[msg.admin.audit.registry]
|
||||||
count = "Count"
|
count = "Count"
|
||||||
|
description = "Filter recent audit logs by search criteria and review action history quickly."
|
||||||
|
|
||||||
[msg.admin.groups]
|
[msg.admin.groups]
|
||||||
|
|
||||||
@@ -937,6 +938,12 @@ user = "User"
|
|||||||
tenant_integrity = "Tenant integrity"
|
tenant_integrity = "Tenant integrity"
|
||||||
user_integrity = "User integrity"
|
user_integrity = "User integrity"
|
||||||
|
|
||||||
|
[msg.admin.integrity.section.tenant_integrity]
|
||||||
|
description = "Checks duplicate tenant slugs and orphan parent relationships."
|
||||||
|
|
||||||
|
[msg.admin.integrity.section.user_integrity]
|
||||||
|
description = "Checks orphan records for users and login ID references."
|
||||||
|
|
||||||
[ui.admin.integrity.check.duplicate_tenant_slugs]
|
[ui.admin.integrity.check.duplicate_tenant_slugs]
|
||||||
title = "Duplicate tenant slug"
|
title = "Duplicate tenant slug"
|
||||||
|
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ empty = "필터 없음"
|
|||||||
|
|
||||||
[msg.admin.audit.registry]
|
[msg.admin.audit.registry]
|
||||||
count = "로드된 로그 {{count}}건"
|
count = "로드된 로그 {{count}}건"
|
||||||
|
description = "최근 감사 로그를 검색 조건에 맞춰 필터링하고, 작업 이력을 빠르게 확인합니다."
|
||||||
|
|
||||||
[msg.admin.groups]
|
[msg.admin.groups]
|
||||||
|
|
||||||
@@ -941,6 +942,12 @@ user = "사용자"
|
|||||||
tenant_integrity = "테넌트 정합성"
|
tenant_integrity = "테넌트 정합성"
|
||||||
user_integrity = "사용자 정합성"
|
user_integrity = "사용자 정합성"
|
||||||
|
|
||||||
|
[msg.admin.integrity.section.tenant_integrity]
|
||||||
|
description = "테넌트 slug 중복과 부모 관계 이상을 확인합니다."
|
||||||
|
|
||||||
|
[msg.admin.integrity.section.user_integrity]
|
||||||
|
description = "사용자와 로그인 ID 참조의 고아 레코드를 확인합니다."
|
||||||
|
|
||||||
[ui.admin.integrity.check.duplicate_tenant_slugs]
|
[ui.admin.integrity.check.duplicate_tenant_slugs]
|
||||||
title = "중복 테넌트 slug"
|
title = "중복 테넌트 slug"
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ const translations: Record<"ko" | "en", Record<string, string>> = {
|
|||||||
"ui.admin.integrity.title": "데이터 정합성 검증",
|
"ui.admin.integrity.title": "데이터 정합성 검증",
|
||||||
"msg.admin.integrity.subtitle":
|
"msg.admin.integrity.subtitle":
|
||||||
"정합성 상태를 확인하고 데이터 모델 전반의 검증 결과를 살펴봅니다.",
|
"정합성 상태를 확인하고 데이터 모델 전반의 검증 결과를 살펴봅니다.",
|
||||||
|
"msg.admin.integrity.section.tenant_integrity.description":
|
||||||
|
"테넌트 slug 중복과 부모 관계 이상을 확인합니다.",
|
||||||
|
"msg.admin.integrity.section.user_integrity.description":
|
||||||
|
"사용자와 로그인 ID 참조의 고아 레코드를 확인합니다.",
|
||||||
|
"msg.admin.audit.registry.description":
|
||||||
|
"최근 감사 로그를 검색 조건에 맞춰 필터링하고, 작업 이력을 빠르게 확인합니다.",
|
||||||
"ui.admin.integrity.recheck.run": "다시 검사",
|
"ui.admin.integrity.recheck.run": "다시 검사",
|
||||||
"ui.admin.integrity.recheck.running": "검사 중",
|
"ui.admin.integrity.recheck.running": "검사 중",
|
||||||
"ui.admin.integrity.status.fail": "실패",
|
"ui.admin.integrity.status.fail": "실패",
|
||||||
@@ -103,6 +109,12 @@ const translations: Record<"ko" | "en", Record<string, string>> = {
|
|||||||
"ui.admin.integrity.title": "Data Integrity Check",
|
"ui.admin.integrity.title": "Data Integrity Check",
|
||||||
"msg.admin.integrity.subtitle":
|
"msg.admin.integrity.subtitle":
|
||||||
"Review integrity status and inspect checks across the admin data model.",
|
"Review integrity status and inspect checks across the admin data model.",
|
||||||
|
"msg.admin.integrity.section.tenant_integrity.description":
|
||||||
|
"Checks duplicate tenant slugs and orphan parent relationships.",
|
||||||
|
"msg.admin.integrity.section.user_integrity.description":
|
||||||
|
"Checks orphan records for users and login ID references.",
|
||||||
|
"msg.admin.audit.registry.description":
|
||||||
|
"Filter recent audit logs by search criteria and review action history quickly.",
|
||||||
"ui.admin.integrity.recheck.run": "Run again",
|
"ui.admin.integrity.recheck.run": "Run again",
|
||||||
"ui.admin.integrity.recheck.running": "Checking",
|
"ui.admin.integrity.recheck.running": "Checking",
|
||||||
"ui.admin.integrity.status.fail": "Failed",
|
"ui.admin.integrity.status.fail": "Failed",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ function cx(...classNames: Array<string | false | null | undefined>) {
|
|||||||
|
|
||||||
type PageHeaderProps = Omit<HTMLAttributes<HTMLElement>, "title"> & {
|
type PageHeaderProps = Omit<HTMLAttributes<HTMLElement>, "title"> & {
|
||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
|
icon?: ReactNode;
|
||||||
as?: ElementType;
|
as?: ElementType;
|
||||||
description?: ReactNode;
|
description?: ReactNode;
|
||||||
eyebrow?: ReactNode;
|
eyebrow?: ReactNode;
|
||||||
@@ -20,6 +21,7 @@ export function PageHeader({
|
|||||||
className,
|
className,
|
||||||
description,
|
description,
|
||||||
eyebrow,
|
eyebrow,
|
||||||
|
icon,
|
||||||
sticky = false,
|
sticky = false,
|
||||||
title,
|
title,
|
||||||
titleAs,
|
titleAs,
|
||||||
@@ -38,6 +40,12 @@ export function PageHeader({
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
<div className="flex min-w-0 items-start gap-3">
|
||||||
|
{icon ? (
|
||||||
|
<div className="mt-1 flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-primary/15 bg-primary/10 text-primary">
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{eyebrow ? (
|
{eyebrow ? (
|
||||||
<p className="text-xs uppercase tracking-[0.2em] text-muted-foreground">
|
<p className="text-xs uppercase tracking-[0.2em] text-muted-foreground">
|
||||||
@@ -51,6 +59,7 @@ export function PageHeader({
|
|||||||
<p className="text-sm text-muted-foreground">{description}</p>
|
<p className="text-sm text-muted-foreground">{description}</p>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{actions ? (
|
{actions ? (
|
||||||
<div className="flex flex-wrap items-center gap-2">{actions}</div>
|
<div className="flex flex-wrap items-center gap-2">{actions}</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
Reference in New Issue
Block a user