diff --git a/adminfront/src/features/audit/AuditLogsPage.tsx b/adminfront/src/features/audit/AuditLogsPage.tsx index 0a067e0e..fe7a60e6 100644 --- a/adminfront/src/features/audit/AuditLogsPage.tsx +++ b/adminfront/src/features/audit/AuditLogsPage.tsx @@ -32,6 +32,14 @@ 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 { AuditLog } from "../../lib/adminApi"; @@ -44,61 +52,6 @@ const defaultAuditFilters = [ "latency_ms:>1000", ]; -type AuditDetails = { - request_id?: string; - method?: string; - path?: string; - status?: number; - latency_ms?: number; - error?: string; - tenant_id?: string; - actor_id?: string; - action?: string; - target?: string; - before?: unknown; - after?: unknown; -}; - -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 formatCellValue(value: unknown) { - if (value === null || value === undefined || value === "") { - return "-"; - } - if (typeof value === "string") { - return value; - } - try { - return JSON.stringify(value); - } catch { - return String(value); - } -} - -function formatIsoDateTime(value: string) { - if (!value) { - return { date: "-", time: "-" }; - } - const parsed = new Date(value); - if (Number.isNaN(parsed.getTime())) { - return { date: value, time: "-" }; - } - const date = parsed.toISOString().slice(0, 10); - const time = parsed.toLocaleTimeString("ko-KR", { hour12: false }); - return { date, time }; -} - function AuditLogsPage() { const [filters, setFilters] = React.useState(defaultAuditFilters); const [filterDraft, setFilterDraft] = React.useState(""); @@ -146,7 +99,7 @@ function AuditLogsPage() { if (isLoading) { return (
- {t("msg.admin.audit.loading", "Loading audit logs...")} + {t("msg.common.audit.loading", "Loading audit logs...")}
); } @@ -157,7 +110,7 @@ function AuditLogsPage() { (error as Error).message; return (
- {t("msg.admin.audit.load_error", "Error loading logs: {{error}}", { + {t("msg.common.audit.load_error", "Error loading logs: {{error}}", { error: errMsg, })}
@@ -169,7 +122,7 @@ function AuditLogsPage() {
- {t("ui.admin.audit.registry.title", "Log Registry")} + {t("ui.common.audit.registry.title", "Audit registry")} {t("msg.admin.audit.registry.count", "총 {{count}}개 로그", { @@ -274,25 +227,15 @@ function AuditLogsPage() { - {t("ui.admin.audit.table.time", "TIME")} + {t("ui.common.audit.table.time", "시간")} - {t("ui.admin.audit.table.actor", "ACTOR (ID)")} - - - {t("ui.admin.audit.table.request", "REQUEST")} - - - {t("ui.admin.audit.table.path", "PATH")} + {t("ui.common.audit.table.actor", "수행자")} + {t("ui.common.audit.table.action", "액션")} + {t("ui.common.audit.table.target", "대상")} - {t("ui.admin.audit.table.status", "STATUS")} - - - {t( - "ui.admin.audit.table.action_target", - "Action / Target", - )} + {t("ui.common.audit.table.status", "상태")} @@ -300,28 +243,26 @@ function AuditLogsPage() { {isLoading && ( - + {t("msg.common.loading", "로딩 중...")} )} {!isLoading && logs.length === 0 && ( - + {t( - "msg.admin.audit.empty", + "msg.common.audit.empty", "아직 수집된 감사 로그가 없습니다.", )} )} {logs.map((row, index) => { - const details = parseDetails(row.details); - const actionLabel = - details.action || - (details.method && details.path - ? `${details.method} ${details.path}` - : row.event_type); + const details = parseAuditDetails(row.details); + const actionLabel = resolveAuditAction(row, details); + const actorLabel = resolveAuditActor(row, details); + const targetLabel = resolveAuditTarget(details); const rowKey = `${row.event_id}-${row.timestamp}-${index}`; const isExpanded = Boolean(expandedRows[rowKey]); return ( @@ -329,7 +270,7 @@ function AuditLogsPage() { {(() => { - const { date, time } = formatIsoDateTime( + const { date, time } = formatAuditDateParts( row.timestamp, ); return ( @@ -343,45 +284,18 @@ function AuditLogsPage() {
- {row.user_id || details.actor_id || "-"} + {actorLabel} - {(row.user_id || details.actor_id) && ( + {actorLabel !== "-" && ( - )} -
-
- -
- - {formatCellValue(details.request_id)} - - {details.request_id && ( - @@ -390,10 +304,26 @@ function AuditLogsPage() {
- {formatCellValue(details.method)} + {actionLabel}
-
- {formatCellValue(details.path)} + + +
+ {targetLabel} + {targetLabel !== "-" && ( + + )}
@@ -407,38 +337,6 @@ function AuditLogsPage() { {row.status} - -
- {actionLabel} -
- {details.target && ( -
- - {t( - "ui.admin.audit.target", - "Target · {{target}}", - { - target: details.target, - }, - )} - - -
- )} -
) : ( diff --git a/common/core/audit/index.ts b/common/core/audit/index.ts new file mode 100644 index 00000000..a313d567 --- /dev/null +++ b/common/core/audit/index.ts @@ -0,0 +1,92 @@ +export type CommonAuditLog = { + event_id: string; + timestamp: string; + user_id: string; + event_type: string; + status: string; + ip_address: string; + user_agent: string; + device_id?: string; + details?: string; +}; + +export type AuditDetails = { + request_id?: string; + method?: string; + path?: string; + status?: number; + latency_ms?: number; + error?: string; + tenant_id?: string; + actor_id?: string; + action?: string; + target?: string; + target_id?: string; + before?: unknown; + after?: unknown; +}; + +export function parseAuditDetails(details?: string): AuditDetails { + if (!details) { + return {}; + } + try { + const parsed = JSON.parse(details); + if (parsed && typeof parsed === "object") { + return parsed as AuditDetails; + } + } catch {} + return {}; +} + +export function formatAuditValue(value: unknown) { + if (value === null || value === undefined || value === "") { + return "-"; + } + if (typeof value === "string") { + return value; + } + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} + +export function formatAuditDateParts(value: string) { + if (!value) { + return { date: "-", time: "-" }; + } + const parsed = new Date(value); + if (Number.isNaN(parsed.getTime())) { + return { date: value, time: "-" }; + } + return { + date: parsed.toISOString().slice(0, 10), + time: parsed.toLocaleTimeString("ko-KR", { hour12: false }), + }; +} + +export function resolveAuditActor( + log: Pick, + details: AuditDetails, +) { + return log.user_id || details.actor_id || "-"; +} + +export function resolveAuditAction( + log: Pick, + details: AuditDetails, +) { + if (details.action) { + return details.action; + } + if (details.method && details.path) { + return `${details.method} ${details.path}`; + } + return log.event_type; +} + +export function resolveAuditTarget(details: AuditDetails) { + return details.target || details.target_id || "-"; +} diff --git a/devfront/src/features/audit/AuditLogsPage.tsx b/devfront/src/features/audit/AuditLogsPage.tsx index b3cfdead..40584bde 100644 --- a/devfront/src/features/audit/AuditLogsPage.tsx +++ b/devfront/src/features/audit/AuditLogsPage.tsx @@ -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 (
- {t("msg.dev.audit.load_error", "Error loading logs: {{error}}", { + {t("msg.common.audit.load_error", "Error loading logs: {{error}}", { error: errMsg, })}
@@ -194,8 +155,8 @@ function AuditLogsPage() { return (
- {t("ui.dev.audit.table.time", "Time")} + {t("ui.common.audit.table.time", "Time")} - {t("ui.dev.audit.table.actor", "Actor")} + {t("ui.common.audit.table.actor", "Actor")} - {t("ui.dev.audit.table.action", "Action")} + {t("ui.common.audit.table.action", "Action")} - {t("ui.dev.audit.table.target", "Target")} + {t("ui.common.audit.table.target", "Target")} - {t("ui.dev.audit.table.status", "Status")} + {t("ui.common.audit.table.status", "Status")} @@ -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...")} ) : 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.")} ) : ( 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 ( - {formatDateTime(row.timestamp)} + {(() => { + const { date, time } = formatAuditDateParts( + row.timestamp, + ); + return ( +
+
{date}
+
{time}
+
+ ); + })()}
- {row.user_id || "-"} - {row.user_id ? ( + {actorLabel} + {actorLabel !== "-" ? ( @@ -411,31 +383,64 @@ function AuditLogsPage() { colSpan={6} className="text-xs text-muted-foreground" > -
+
-
+
+ Request +
+
Request ID:{" "} - {formatValue(details.request_id)} + {formatAuditValue(details.request_id)} +
+
+ Event ID:{" "} + {formatAuditValue(row.event_id)} +
+
IP: {formatAuditValue(row.ip_address)}
+
+ Method: {formatAuditValue(details.method)} +
+
+ Path: {formatAuditValue(details.path)}
- Method: {formatValue(details.method)} + Latency:{" "} + {details.latency_ms !== undefined + ? `${details.latency_ms}ms` + : "-"} +
+
+
+
+ Actor +
+
Actor ID: {actorLabel}
+
+ Tenant:{" "} + {formatAuditValue(details.tenant_id)}
- Path: {formatValue(details.path)} + Device:{" "} + {formatAuditValue(row.device_id)}
-
- Tenant: {formatValue(details.tenant_id)} +
+ Target: {targetValue}
-
- Before: {formatValue(details.before)} +
+ Result
- After: {formatValue(details.after)} + Error:{" "} + {formatAuditValue(details.error)}
- Error: {formatValue(details.error)} + Before:{" "} + {formatAuditValue(details.before)} +
+
+ After: {formatAuditValue(details.after)}
@@ -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")}
) : null}