1
0
forked from baron/baron-sso

감사 로그 테이블 공통 컬럼 통일

This commit is contained in:
2026-05-15 13:26:08 +09:00
parent 94f33a0a64
commit 055a804f7f
3 changed files with 255 additions and 254 deletions

View File

@@ -30,59 +30,20 @@ import {
TableHeader,
TableRow,
} from "../../components/ui/table";
import {
formatAuditDateParts,
formatAuditValue,
parseAuditDetails,
resolveAuditAction,
resolveAuditActor,
resolveAuditTarget,
} from "../../../../common/core/audit";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import { PageHeader } from "../../../../common/core/components/page";
import type { DevAuditLog } from "../../lib/devApi";
import { fetchDevAuditLogs } from "../../lib/devApi";
import { t } from "../../lib/i18n";
type AuditDetails = {
request_id?: string;
method?: string;
path?: string;
tenant_id?: string;
action?: string;
target_id?: string;
before?: unknown;
after?: unknown;
error?: string;
};
function parseDetails(details?: string): AuditDetails {
if (!details) {
return {};
}
try {
const parsed = JSON.parse(details);
if (parsed && typeof parsed === "object") {
return parsed as AuditDetails;
}
} catch {}
return {};
}
function formatValue(value: unknown): string {
if (value === null || value === undefined || value === "") {
return "-";
}
if (typeof value === "string") {
return value;
}
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}
function formatDateTime(value: string): string {
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) {
return value;
}
return parsed.toLocaleString("ko-KR");
}
function toCsv(logs: DevAuditLog[]) {
const header = [
"timestamp",
@@ -95,7 +56,7 @@ function toCsv(logs: DevAuditLog[]) {
"request_id",
];
const rows = logs.map((logItem) => {
const details = parseDetails(logItem.details);
const details = parseAuditDetails(logItem.details);
return [
logItem.timestamp,
logItem.user_id || "",
@@ -184,7 +145,7 @@ function AuditLogsPage() {
axiosError.response?.data?.error ?? (query.error as Error).message;
return (
<div className="p-8 text-center text-red-500">
{t("msg.dev.audit.load_error", "Error loading logs: {{error}}", {
{t("msg.common.audit.load_error", "Error loading logs: {{error}}", {
error: errMsg,
})}
</div>
@@ -194,8 +155,8 @@ function AuditLogsPage() {
return (
<div className="space-y-6">
<PageHeader
eyebrow={t("ui.dev.audit.registry.title", "Audit registry")}
title={t("ui.dev.audit.title", "Audit Logs")}
eyebrow={t("ui.common.audit.registry.title", "Audit registry")}
title={t("ui.common.audit.title", "Audit Logs")}
description={t(
"msg.dev.audit.subtitle",
"Shows DevFront activity history within current tenant/app scope.",
@@ -291,19 +252,19 @@ function AuditLogsPage() {
<TableHeader className={commonStickyTableHeaderClass}>
<TableRow>
<TableHead className="w-[190px]">
{t("ui.dev.audit.table.time", "Time")}
{t("ui.common.audit.table.time", "Time")}
</TableHead>
<TableHead className="w-[180px]">
{t("ui.dev.audit.table.actor", "Actor")}
{t("ui.common.audit.table.actor", "Actor")}
</TableHead>
<TableHead className="w-[180px]">
{t("ui.dev.audit.table.action", "Action")}
{t("ui.common.audit.table.action", "Action")}
</TableHead>
<TableHead className="w-[260px]">
{t("ui.dev.audit.table.target", "Target")}
{t("ui.common.audit.table.target", "Target")}
</TableHead>
<TableHead className="w-[120px]">
{t("ui.dev.audit.table.status", "Status")}
{t("ui.common.audit.table.status", "Status")}
</TableHead>
<TableHead className="w-[80px]" />
</TableRow>
@@ -315,7 +276,7 @@ function AuditLogsPage() {
colSpan={6}
className="py-8 text-center text-muted-foreground"
>
{t("msg.dev.audit.loading", "Loading audit logs...")}
{t("msg.common.audit.loading", "Loading audit logs...")}
</TableCell>
</TableRow>
) : logs.length === 0 ? (
@@ -324,31 +285,42 @@ function AuditLogsPage() {
colSpan={6}
className="text-center text-muted-foreground"
>
{t("msg.dev.audit.empty", "No audit logs found.")}
{t("msg.common.audit.empty", "No audit logs found.")}
</TableCell>
</TableRow>
) : (
logs.map((row, index) => {
const details = parseDetails(row.details);
const actionLabel = details.action || row.event_type;
const targetValue = details.target_id || "-";
const details = parseAuditDetails(row.details);
const actionLabel = resolveAuditAction(row, details);
const actorLabel = resolveAuditActor(row, details);
const targetValue = resolveAuditTarget(details);
const rowKey = `${row.event_id}-${row.timestamp}-${index}`;
const expanded = Boolean(expandedRows[rowKey]);
return (
<React.Fragment key={rowKey}>
<TableRow>
<TableCell className="text-xs text-muted-foreground">
{formatDateTime(row.timestamp)}
{(() => {
const { date, time } = formatAuditDateParts(
row.timestamp,
);
return (
<div className="space-y-1">
<div>{date}</div>
<div>{time}</div>
</div>
);
})()}
</TableCell>
<TableCell className="font-mono text-xs">
<div className="flex items-center gap-2">
<span>{row.user_id || "-"}</span>
{row.user_id ? (
<span>{actorLabel}</span>
{actorLabel !== "-" ? (
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-muted-foreground"
onClick={() => handleCopy(row.user_id)}
onClick={() => handleCopy(actorLabel)}
>
<Copy className="h-3 w-3" />
</Button>
@@ -411,31 +383,64 @@ function AuditLogsPage() {
colSpan={6}
className="text-xs text-muted-foreground"
>
<div className="grid gap-3 md:grid-cols-2">
<div className="grid gap-4 md:grid-cols-3">
<div className="space-y-1">
<div>
<div className="uppercase tracking-[0.16em]">
Request
</div>
<div className="break-all">
Request ID:{" "}
{formatValue(details.request_id)}
{formatAuditValue(details.request_id)}
</div>
<div className="break-all">
Event ID:{" "}
{formatAuditValue(row.event_id)}
</div>
<div>IP: {formatAuditValue(row.ip_address)}</div>
<div className="break-all">
Method: {formatAuditValue(details.method)}
</div>
<div className="break-all">
Path: {formatAuditValue(details.path)}
</div>
<div>
Method: {formatValue(details.method)}
Latency:{" "}
{details.latency_ms !== undefined
? `${details.latency_ms}ms`
: "-"}
</div>
</div>
<div className="space-y-1">
<div className="uppercase tracking-[0.16em]">
Actor
</div>
<div>Actor ID: {actorLabel}</div>
<div>
Tenant:{" "}
{formatAuditValue(details.tenant_id)}
</div>
<div>
Path: {formatValue(details.path)}
Device:{" "}
{formatAuditValue(row.device_id)}
</div>
<div>
Tenant: {formatValue(details.tenant_id)}
<div className="break-all">
Target: {targetValue}
</div>
</div>
<div className="space-y-1 break-all">
<div>
Before: {formatValue(details.before)}
<div className="uppercase tracking-[0.16em]">
Result
</div>
<div>
After: {formatValue(details.after)}
Error:{" "}
{formatAuditValue(details.error)}
</div>
<div>
Error: {formatValue(details.error)}
Before:{" "}
{formatAuditValue(details.before)}
</div>
<div>
After: {formatAuditValue(details.after)}
</div>
</div>
</div>
@@ -461,7 +466,7 @@ function AuditLogsPage() {
>
{query.isFetchingNextPage
? t("msg.common.loading", "Loading...")
: t("ui.dev.audit.load_more", "Load more")}
: t("ui.common.audit.load_more", "Load more")}
</Button>
</div>
) : null}